\title{
A Framework for Developing Finite Element Codes for Multi-Disciplinary Applications\\
为多学科应用开发有限元代码的框架
}

\author{
Pooyan Dadvand \\ 
翻译： Jiaqi-2021-8-10
}



\begin{abstract}。
近年来，计算模拟的世界经历了巨大的进步，需要更多的多学科挑战来满足即将到来的新需求。解决多学科问题的重要性不断增加，这使得开发人员更加关注这些问题，并处理好在这一领域开发软件所涉及的困难。

传统的有限元代码在处理多学科问题时有一些困难。许多这些代码是为解决某种类型的问题而设计和实施的，通常涉及单一领域。将这些代码扩展到处理另一个领域的分析，通常包括几个问题和大量的修改和实现。一些典型的困难是：每个节点预定义的自由度集，具有固定定义变量集的数据结构，所有实体的全局变量列表，基于领域的接口，读取新数据和写入新结果的IO限制以及代码内的算法定义。一个常见的方法是通过一个主程序连接不同的求解器，该程序实现了交互算法，也将数据从一个求解器转移到另一个求解器。这种方法在实践中得到了成功的应用，但其结果是重复的实施和多余的数据存储和传输的开销，这可能会对求解器的数据结构产生重大影响。

这项工作的目的是设计和实现一个框架来建立多学科的有限元程序。通用性、可重用性、可扩展性、良好的性能和内存效率被认为是设计和实现该框架的要点。为团队开发准备结构是另一个目标，因为通常不同领域的专家团队会参与多学科代码的开发。

Kratos ，在这项工作中创建的框架提供了几个工具，以方便实现有限元的应用，也为其应用的不同方式的自然互动提供了一个共同平台。这不仅是通过一些创新来实现的，也是通过收集和重用几个现有的作品来实现的。

在这项工作中，设计并实现了一个创新的变量基础接口，它被用于不同的抽象层次，并显示出非常清晰和可扩展。另一个创新是一个非常有效和灵活的数据结构，它可以用来以类型安全的方式存储任何类型的数据。一个可扩展的IO也被创建，以克服处理多学科问题的另一个瓶颈。收集现有工作的不同概念并使其适应耦合问题被认为是这项工作的另一个创新。例如，使用解释器、不同的数据组织和每个节点的可变自由度数量。内核和应用的方法被用来减少不同领域的开发者之间可能产生的冲突，层的设计反映了不同开发者的工作空间，也考虑了他们的编程知识。最后，为了提高Kratos的性能和效率，应用了一些技术细节，这使得它可以实际使用。

这项工作通过在实践中演示该框架的功能而完成。 首先，实现了一些经典的单一领域的应用，如热、流体和结构应用，并作为基准来证明其性能。这些应用被用来解决耦合问题，以展示该框架提供的自然交互设施。最后实现了一些不太经典的耦合有限元算法，以显示其高度的灵活性和可扩展性。
\end{abstract}







\section{Introduction} 

汽车、航空航天和建筑部门传统上使用仿真程序来改进他们的产品或服务，将计算集中在几个主要的物理过程：流体动力学、结构、金属成型等。然而，对更安全、更便宜和更有效的产品的新需求，以及计算设施的惊人增长，要求对多学科问题有一个同等的解决方案。此外，在食品、化学和电磁工业等领域的新应用，如果没有多学科的方法，根本就没有用。

一些说明性的案例可以在风帆和帆船的设计中找到，在这些设计中必须考虑到结构和流体动力学计算、消毒过程（热和流体动力学分析）、强磁设计（结构、热和磁分析）或光伏电池（热和电计算）等等。

解决多学科问题的重要性日益增加，使得开发人员更加关注这些问题，并处理在该领域开发软件所涉及的困难。

\subsection{Motivation} 

近年来，计算模拟的世界经历了巨大的进步，需要更多的多学科挑战来满足新的需求。

由于不同领域的单用途代码在工业上的成熟，生产部门已经提高了期望值，因为他们意识到需要以更现实的方式解决他们处理的问题，以保持竞争力。强烈的简化是过去困难问题得以解决的原因。这些简化导致的模型与真实的模型有很大的不同，就像所做的假设的严重程度一样。如果我们在一个并发的世界中增加每一个所需的精度，那么我们就需要放松假设，在解决多学科问题的方式上变得更加普遍。

这种情况其实并不新鲜。新颖的是解决多学科问题的需要，结合来自不同领域的大部分信息，获得更精确的信息，从而获得更好的优化方法。这意味着要解决更复杂的问题。有许多策略来解决这类问题。一个简单的方法是假设现有的分析方法是好的，人们应该在现有的代码之间建立接口，以解决多学科的问题。不幸的是，这种策略对于高度耦合问题的单体解决方案来说根本不起作用，而且由于数据格式转换和重复数据产生的内存开销，通常会有执行时间的开销。这些缺点也正是为创建一个多学科编程框架提供了强大的动力，该框架将允许以一种更自然和灵活的方式处理这类问题。

此外，它还计划在多学科解决方案中整合另一个有趣的主题，如优化。用于解决工业优化问题的大量研究工作构成了提供一个负担得起的库作为优化程序的数字引擎的强大动力。

\subsection{Problem} 

如今有限元方法的相关主题之一是将不同的分析（热、流体动力学、结构）与优化方法结合起来，并在一个全局软件包中进行自适应网格划分，只需一个用户界面，并有可能将实现的解决方案扩展到新的问题类型，作为一种多学科仿真环境的方法。

传统的有限元代码在处理涉及两个或多个不同领域（即流体-结构、热-机械、热-流体等）的耦合解决方案的多学科问题时遇到了一些困难。许多这些代码是为解决某种类型的问题而设计和实施的，通常涉及单一领域。例如，只解决结构、流体或电磁问题。将这些代码扩展到处理另一个领域的分析，通常需要大量的修改。

一个常见的方法是通过一个主程序连接不同的求解器，该程序实现了交互算法，也将数据从一个求解器传输到另一个求解器。这种方法在实践中已被成功使用，但也有其自身的问题。 首先，所有的完全独立的程序导致许多重复的实现，这在代码维护方面造成了额外的成本。在许多情况下，不同求解器之间的数据传输不是微不足道的，而且根据每个程序的数据结构，可能会造成将数据从一个数据结构复制到另一个数据结构的冗余开销。最后，这种方法只能用于主从耦合，不能用于单体耦合方案。

一些较新的实现方法在设计中使用了现代软件工程的概念，以使程序更具可扩展性。他们通常在程序中实现了对新算法的可扩展性，但将其扩展到新的领域则超出了他们的本意。

使用面向对象的范式有助于提高代码的可重复使用性。这被认为是简化解决新类型问题的模块实施的一个关键点。不幸的是，在他们的设计中，许多特定领域的概念限制了他们对其他模块的重用性。

现有的处理多学科问题的代码的典型瓶颈是。

- 预先定义的每个节点的自由度集合。

- 具有固定定义变量集的数据结构。

- 所有实体的全局变量链表。

- 基于接口的Domain定义。

- IO读取新数据和写入新结果时的限制。

- 代码内部的算法定义。

这些缺点需要对代码进行大量的重写，以便将其扩展到新的领域。许多程序对每个节点的自由度有一个预定义的集合。例如，在一个三维结构程序中，每个节点有六个自由度$d_{x}, d_{y}, d_{z}, w_{x}, w_{y}, w_{z}$，其中$d$是节点位移，$w$是节点旋转。假设所有的节点都只有这一组自由度，有助于开发者优化他们的代码，也简化了他们的实施任务。但是这个假设阻碍了将代码扩展到每个节点具有不同自由度集合的其他领域。

通常情况下，程序的数据结构被设计为持有某些变量和历史值。这种设计的主要原因是：更容易实现，数据结构的性能更好，维护起来更省力。尽管有这些优点，使用刚性的数据结构通常需要进行重要的修改，以容纳来自其他领域的新变量。

当程序的数据结构被设计为为所有实体保存相同的数据集时，就会出现另一个问题。在这种情况下，向数据结构中添加一个节点变量意味着向模型中的所有节点添加这个变量。在这种实现中，在数据结构中添加一个域的变量会导致在另一个域中为其分配多余的空间。例如在一个流体与结构的相互作用问题中，这种设计导致每个结构节点都有压力值，所有流体节点都有位移值存储在内存中。尽管这不是一个限制，但它严重影响了程序的内存效率。

此外，在单用途程序中，为了提高代码的清晰度，通常会创建特定领域的接口。例如，为元素的属性提供一个`Conductivity`方法来获取元素的电导率，如下所示。

~~~C++
c = properties.Conductivity ()
~~~


虽然这提高了代码的清晰度，但它与对新领域的扩展性完全不相容。

 IO是扩展程序到新字段的另一个瓶颈。每个字段都有其数据和结果集，简单的IO通常无法处理新的数据集。这可能会造成巨大的实施和维护成本，这些成本来自于为每个新问题的解决而更新IO。

最后在现有代码中引入新的算法需要内部实现。这导致封闭的程序是不可扩展的，因为无法访问其源代码。对于开放源码程序，这需要外部开发者了解代码的内部结构。

\subsection{Objectives} 

这项工作的目的是设计和实现一个框架，用于构建多学科的有限元程序。这个框架被称为Kratos，将有助于建立各种各样的有限元程序，从最简单的公式，例如热传导问题，到最复杂的，如多学科的优化技术。从一方面来说，它将提供一集完整的灵活的工具来快速实现实验性的学术算法，从另一方面来说，它必须是快速有效的，以用于真正的工业分析。

通用性是我们设计的第一个目标。如果一个框架的通用性足以适应各种各样的有限元算法，它就可以被使用。当涉及到多学科分析时，通用性变得更加重要，代码结构必须支持不同领域的各种算法。

可重用性是这个设计的另一个目标。有限元方法有几个步骤在不同的算法之间是相似的，即使是对不同类型的问题。一个好的设计和实现可以使所有这些步骤在所有的算法中都可以重复使用。这就减少了实施工作和代码的维护成本。

另一个目标是为 Kratos 提供高水平的可扩展性。有限元方法论的不断创新可能导致未来出现具有完全不同要求的新算法。因此，仅仅支持大量的当前算法不能保证代码在未来的实用性。解决办法是提供一些方法，将代码的不同部分扩展到新的情况。对于设计一个多学科的代码，可扩展性起着更重要的作用。多学科代码的可扩展性对于支持不同领域的新问题也是必要的。所以可扩展性被认为是 Kratos 的一个重要目标。

良好的性能和内存效率也是 Kratos 的目标。解决多学科的工业问题是Kratos的目标，需要良好的性能和良好的内存效率水平。通用性和灵活性不利于代码的性能和效率。所以很明显，Kratos不能达到完全优化的单域代码的相同性能。但其想法是保持尽可能快的速度，同时通过减少域之间的数据转换和传输来提高解决多学科问题的总性能。

\subsection{Solutions} 

应用面向对象的范式已被证明对提高代码的灵活性和可重用性非常有帮助。在这项工作中，面向对象的设计被成功地用于将代码的不同部分组织在一集具有明确界面的对象中。这样一来，用一个对象替换另一个对象就非常容易，这就增加了代码的灵活性，而且在其他地方重复使用一个对象也变得更加实用。

设计和实现一个多学科的概念界面是为以前的问题提供的另一个解决方案。在Kratos的设计中，接口是以一种非常通用的方式定义的，与任何具体的概念无关。这种设计产生的变量基础接口非常通用，解决了将程序扩展到新领域时产生的接口问题。

一个灵活和可扩展的数据结构是另一个用来保证代码对新概念的可扩展性的解决方案。建议的数据结构能够存储与任何概念相关的任何类型的数据。它还为处理多领域问题时所需的数据的全局组织提供了不同的方法。同样的策略被应用于为任何节点分配任何的自由度集以解决新问题的灵活性上。

为了在实现不同算法时增加代码的灵活性，提供了一个交互式界面。通过这种方式可以引入新的算法，而不需要在程序内部实现。这使代码具有高度的可扩展性，对于实现优化和多学科的交互算法非常有用。

一个可自动配置的IO模块被添加到这些组件中，提供处理多学科问题所需的完整的一套解决方案。这个IO模块使用不同的组件列表，在读写源自不同分析领域的新概念时进行自我调整。


\subsection{Organization} 

这项工作提出了一个新的面向对象的有限元框架的设计。其想法是提出其设计的一般观点，然后将设计程序分为其主要部分，同时在单独的章节中对每个部分进行单独描述。按照这一思路，工作的布局安排如下:

\textbf{Chapter 2: Background} 在这一章中，给出了有限元编程的背景。

\textbf{Chapter 3: Concepts} 本章首先简要介绍了数值分析的一般情况，然后继续详细解释了有限元方法及其概念。最后，它描述了整个工作中使用的不同设计模式，并简要介绍了用于编写高性能数值应用程序的一些高级$\mathrm{C}++$技术。

\textbf{Chapter 4: General Structure} 解释了Kratos的面向对象设计，它的主要对象，代码组织以及内核和应用程序之间的分离。

\textbf{Chapter 5: Basic Tools} 本章介绍了为帮助开发者实现其程序而提供的不同工具。解释了正交方法、线性求解器和几何图形的设计，并提到了其可重用性和通用性的关键点。

\textbf{Chapter 6: Variable Base Interface (VBI)} 介绍了一个新的变量基础接口，可在代码的不同层次上使用。 首先，给出了设计新接口的动机。然后，描述了这个新的接口，并解释了一些应用。之后，有一节是关于它的实现和使用这种方法设计接口的方法。最后，还给出了一些例子来说明VBI在实践中的能力。

\textbf{Chapter 7: Data Structure} 本章首先解释了容器编程的基本概念，并继续描述了不同的容器。然后介绍了为有限元编程设计的新容器。最后，提供了Kratos中数据分配的全局方案。

\textbf{Chapter 8: Finite Element Implementation} 重点介绍了代表 Kratos 中实现的有限元方法的组件。它描述了元素和条件的设计，还解释了 Kratos 中过程和策略的结构。最后，它解释了元素表达式和公式及其设计的目的。

\textbf{Chapter 9: Input Output} 首先，它解释了设计IO模块的不同方法，并提出了一个灵活和通用的IO结构。然后，它继续介绍了专门用于编写解释器的部分，其中包括对概念的少量介绍，以及对相关工具和库的使用的简要解释。最后，它描述了使用Python作为解释器的情况，并对使用Python的原因作了一些说明，该接口库和一些例子显示了其强大的能力。

\textbf{Chapter 10: Validation Examples} 它给出了一些用 Kratos 实现的多学科问题的不同有限元应用的例子。它还包括一些基准来比较Kratos与现有程序的效率。

\textbf{Chapter 11: Conclusions and Future Work} 概述了解决方案及其在解决多学科问题方面的有效性，并解释了本项目的未来方向。


\section{Background} 

面向对象的有限元程序设计的历史可以追溯到90年代初甚至更早。1990年，Forde等人[^44]发表了关于将面向对象的设计应用于有限元程序的最早的详细描述之一。他们使用有限元方法的基本组成部分来设计主要对象。他们的结构包括`Node`、`Element`、`DispBC`、`ForcedBC`、`Material`和`Dof`作为有限元组件和一些附加对象，如`Gauss`和`ShapeFunction`用于协助数值分析。他们还引入了元素组以及节点和元素的`list`来管理网格和构建系统。他们的方法已经被一些作者在组织他们的面向对象的有限元程序结构时重新使用。这种方法专注于结构领域，对象的接口反映了这种依赖性。

在那些年里，其他作者开始撰写关于面向对象范式及其在有限元编程中的应用。Filho和Devloo[^43]对面向对象的设计进行了介绍，将其应用于元素设计。Mackie[^64]通过设计一个简单的分层元素，对有限元程序的面向对象设计做了另一个介绍。后来他发表了一个更详细的有限元程序结构，为实体提供了一个数据结构，还介绍了迭代器的使用 [^65] 。Pidaparti和Hudli [^80] 发表了一个更详细的面向对象的结构，为结构的动态分析中的不同算法提供对象。Raphael和Krishnamoorthy [^82]也对面向对象的编程进行了介绍，并为结构应用提供了复杂的元素层次结构。他们还设计了一些数值表达式来处理有限元方法中的常见数值操作。所有这些作者的共同点是他们认识到了面向对象编程相对于传统Fortran方法的优势，并打算在他们的有限元代码中使用这些优势。

Miller[^70][^71][^72]发表了一个面向对象的非线性动态有限元程序的结构。他在设计中引入了无坐标方法，定义了几何类，处理所有从局部坐标到全局坐标的转换。他设计的主要对象是 `Dof` 、 `Joint` 和 `Element` 。 `TimeDependentLoad`和`Constrain`被添加到其中，以处理边界条件。他还定义了一个`Material`类，能够处理线性和非线性材料。Assemblage类持有所有这些组件并封装了结构的时间历史建模。

Zimmermann等人[^106][^35][^34]设计了一个用于线性动态有限元分析的结构。他们的结构由三类对象组成。 首先，有限元方法对象，它们是: `Node` , `Element` , `Load` , `LoadTimeFunction` , `Material` , `Dof` , `Domain` , 和 `LinearSolver` 。其次，是一些工具，如 `GaussPoint` , 和 `Polynomial` 。第三，是集合类，如`Array` , `Matrix` , `String` , 等等。他们首先在Smalltalk中实现了这个结构的原型，然后在$\mathrm{C}++$中实现了一个高效的结构，后者提供了与Fortran代码相当的性能。

在他们的结构中，Element计算刚度矩阵$K^{e}$、质量矩阵$M^{e}$，以及全局坐标下的载荷向量$f^{e}$。它还在全局方程组中集合其组成部分，并在求解后更新自己。 `Node`保持其位置并管理自由度。它还计算和组装其载荷向量，最后在解算后更新与时间有关的数据。 `Dof`保存未知信息和其值。它还存储其在全球系统中的位置，并提供有关边界条件的信息。TimeStep实现了时间离散化。 `Domain`是一个总管理器，管理节点和元素等组件，也管理求解过程。它还为读取数据和写入结果提供了输入输出功能。最后`LinearSystem`持有方程组组件：左手边、右手边和解。它还执行了方程编号，并实现了解决全局方程组的解算器。

他们还对其结构进行了非线性扩展，这使得他们重新定义了一些原有的类，如 `Domain` 、 `Element` 、 `Material` ，以及一些 `LinearSystems` [^69] 。

Lu等人 [^61][^62] 在他们的有限元代码中提出了一个不同的结构 $\mathrm{FE}++$ 。他们的结构由少量的有限元组件组成，如`Node`、`Element`、`Material`和`Assemble`，是在一个复杂的数值库上设计的。在他们的设计中，`Assemble`是中心对象，不仅实现了正常的装配程序，而且还负责坐标转换，这在其他方法中是元素的职责之一。它还负责分配方程编号。`Element`是他们对新配方的扩展点。他们在实现数值部分的努力导致了一个相当于LAPACK的面向对象的线性代数库 [^15] 。这个库提供了一个使用面向对象语言特征的高级接口。

Archer等人[^17][^18]提出了另一种面向对象的结构，用于专门模拟线性和非线性、静态和动态结构的有限元程序。他们回顾了当时不同的其他设计所提供的功能，并将它们结合到一个新的结构中，同时加入了一些新的概念。

他们的设计由两个层次的抽象组成。在顶层，`Analysis` 封装了算法，`Model`代表有限元组件。 `Map`将模型中的自由度与分析中的未知数联系起来，并消除了这些对象之间的依赖关系。它还将刚度矩阵从元素的局部坐标转换为全局坐标，并计算响应。除了这三个对象之外，不同的处理程序被用来处理算法中与模型相关的部分。`ReorderHandler`优化了未知数的顺序。`MatrixHandler`提供不同的矩阵并在给定的模型上构建它们。最后ConstraintHandler提供了分析的未知数和模型中的自由度之间的初始映射。

在另一个层面，有不同的有限元组件代表模型。 `Node`封装了一个通常的有限元节点，持有其位置和自由度。 `ScalarDof`和`VectorDof`派生自`Dof`类，代表不同的自由度类型。 `Element`使用`ConstitutiveModel`和`ElementLoad`来计算本地坐标系中的刚度矩阵$K^{e}$、质量矩阵$M^{e}$、阻尼矩阵$C^{e}$。 $LoadCase$由载荷、规定位移和初始元素状态对象组成，并计算本地坐标系中的载荷`vector`。

Cardona, et al. [^25][^53][^54] 开发了 `Object Oriented Finite Elements` 由交互式执行器（OOFELIE）引导的方法 [^77] 。他们设计了一种高级语言，还实现了一个解释器来执行该语言中的输入。这种方法使他们能够开发一个非常灵活的工具来处理不同的有限元问题。在他们的结构中，一个`Domain`类持有数据集，如。 `Nodeset`, `Elemset`, `Fixaset`, `Loadset`, `Materset`, 和 `Dofset` 。 `Element` 提供了计算刚度矩阵 $K^{e}$ 、质量矩阵 $M^{e}$ 等的方法。 `Fixaset`和`Loadset`提供了计算边界条件和载荷的Fixations和Loads的方法。

他们把这个灵活的工具也用于解决耦合问题，他们的高级语言解释机制在处理耦合问题的不同算法方面提供了额外的灵活性。他们还在其结构中加入了分区和连接类，以增加其代码在处理和组织耦合问题数据方面的功能。Partition被定义为处理领域的一部分，Connection提供了自由度的图形并对它们进行排序。

Touzani [^96] 开发了Object Finite `Element` Library（OFELI） [^95] 。这个库有一个基于有限元方法的直观结构，可用于开发不同领域的有限元程序，如传热、流体流动、固体力学和电磁。

 `Node`, `Element`, `Side`, `Material`, `Shape`和`Mesh`是其结构的主要组成部分，不同的问题解决器类实现算法。这个库还提供了不同的类，这些类以`FEEqua`类的形式衍生，实现了不同领域的分析公式。它使用了一个静态的`Material`类，其中每个参数被存储为成员变量。`Element`只提供几何信息，有限元的实现是通过`FEEqua`类封装的。`Element`提供了一些功能，使其对复杂的公式也很有用，但保留所有这些成员数据使其对标准的工业实现来说过于沉重。

Bangerth等人 [^22][^19][^21] 创建了一个用于实现偏微分方程的自适应网格化有限元求解的库，名为微分方程分析库（DEAL）II [^20] 。他们关注的是库的灵活性、效率和类型安全，同时也希望实现高水平的抽象性。所有这些要求使他们使用$\mathrm{C}++$语言的高级功能来共同实现他们的所有目标。

他们的方法是在一个域上解决一个偏微分方程。 `Triangulation`、`FiniteElement`、`DoFHandler`、`Quadrature`、`Mapping`和`FEValues`是这个结构中的主要类。 `Triangulation` 尽管它的名字是一个网格生成器，它可以根据给定的尺寸作为模板参数创建线段、四边形和六面体。它还提供了一个有规律的细化单元，并保留了网格的分层历史。`FiniteElement` 封装了形状函数空间。它计算 `FEValues` 的形状函数值。 `Quadrature` 提供不同阶数的单元格上的四分法。 `Mapping` 负责坐标转换。 `DoFHandler` 管理自由度的布局，以及它们在三角测量中的编号。 `FEValues`封装了将在域上使用的公式。它使用`FiniteElement`、`Quadrature`和`Mapping`来进行计算，并提供本地组件以组装成全局求解器。

广泛使用模板和$\mathrm{C}++$编程语言的其他高级功能，在不牺牲其性能的情况下增加了该库的灵活性。他们创建了抽象的类，以便统一处理不同尺寸的几何体。通过这种方式，他们让用户以独立于尺寸的方式创建他们的配方。他们的方法还包括在 $\mathrm{C}++$ 中实现公式和算法，有时也包括模型本身。通过这种方式，库可以更好地根据问题配置自己，并获得更好的性能，但由于要求它作为一个封闭的包来使用，所以降低了程序的灵活性。在他们的结构中，没有像节点、元素、条件等通常的有限元组件的痕迹。这使得具有一般有限元背景的开发者不太熟悉它。

Patzák等人[^79]发表了一个用于面向对象的有限`Element`建模（OOFEM）[^78]程序中的结构。在这个结构中`Domain`包含了完整的问题描述，它可以分为几个工程模型，为解决方程系统做准备。数值方法封装了解决方案的算法。 `Node`, `Element`, `DOF`, `Boundary condition`, `Initial condition`，以及`Material`是这个结构的其他主要对象。该程序以结构分析为导向。

\subsection{Discussion} 

在组织有限元代码方面做了大量工作，试图提高其灵活性并降低维护成本。在文献中可以追踪到两类有限元程序的设计。一类是使用有限元方法进行设计，这导致了诸如元素、节点、网格等对象。另一种方法是处理偏微分方程，导致对象函数与域上的矩阵和向量一起工作。

Zimmermann, et al. [^106][^35][^34]的工作是设计代码结构的经典方法之一，考虑了有限元方法。然而，在他们的设计中没有几何学，也没有涉及像进程、命令解释器等新组件。

Miller等人的努力是为了将坐标转换封装在几何学中，这对于放松元素公式对特定几何学的依赖很有帮助。

Cardona等人[^25][^53][^54]增加了一个解释器，以一种非常灵活的方式管理代码的全局流。解释器用于向程序引入新的求解算法而不改变其内部。这段代码也被扩展到解决耦合问题，使用解释器来引入交互算法，这给它带来了很大的灵活性。他们的方法的缺点是在绑定现有的解释器的同时，还要用新定义的语言来实现一个新的解释器。这意味着解释器本身的维护成本，并阻止他们使用其他可能有接口的脚本语言的库。

Touzani [^96]为多学科的有限元程序设计了一个结构。他的设计清晰易懂，但对其组件使用了特定领域的接口，这些接口对所有领域都不统一。这降低了算法从一个领域到另一个领域的可重复使用性。

Lu, et al. [^61][^62]的方法是在设计偏微分求解器的路线上，强调了数值组件。Archer等人[^17][^18]将这种方法扩展到一个更灵活和可扩展的点上，最近Bangerth等人在设计他们的代码时使用了同样的方法。然而这种设计的结构结果对于通常的有限元程序员来说可能是不熟悉的。例如，在后者的设计中，没有代表节点和元素的对象，而节点和元素是有限元程序员的常用组件。

在这项工作中，标准的有限元对象，如节点、元素、条件等，都是从以前的设计中重复使用的，但经过修改，适应了多学科的视角。此外，还增加了一些新的组件，如模型部分、过程、内核和应用，以涵盖多学科问题中出现的新概念。设计了一个新的变量基础接口，提供了字段之间的统一接口，增加了代码的可重复使用性。使用解释器的想法是通过使用现有的解释器来实现的。还做了大量的工作来设计和实现一个快速和非常灵活的数据结构，使其能够存储来自不同领域的任何类型的数据，并保证数据的统一转换。还创建了一个可扩展的IO，以完成处理多学科问题时通常遇到的瓶颈的工具。





\section{Concepts} 
这项工作的目的是创建一个框架来实现多学科的有限元应用。在开始之前，有必要解释一下有限元方法本身的一些基本概念，多学科问题及其解决方案，以及与其设计和实现有关的编程概念。

在本章中，首先对有限元的概念进行了简要介绍。然后描述了耦合系统的一些基本概念。最后解释了一些编程概念和技术。

\subsection{Numerical Analysis} 

在这一节中，将简要介绍一般的数值分析，并简要介绍不同的数值方法，它们的相似之处和不同之处。

\subsubsection{Numerical Analysis Scheme} 

有几种数值分析方法在其方法和应用类型上是不同的。除了它们之间的差异，它们还依赖于一个总体方案，这使得它们在整体方法上是相似的，并且在某种程度上是可以混合或互换的。这个总体方案由以下三个主要步骤组成。

\textbf{Idealization}：定义一个反映物理系统的数学模型。由于这个原因，这个步骤也被称为 `mathematical modeling` 。在这一步骤中，物理系统的管理方程及其条件被转化为一些可以用数字解决的一般形式。通常情况下，不同的假设是必要的，以使这种建模成为可能。这些假设使模型与物理问题不同，并在我们的解决方案中引入`modeling error`。

\textbf{Discretization}：将具有无限数量未知数的数学模型，称为`degrees of freedom`（自由度），转换为有限数量的未知数。虽然具有无限数量未知数的原始模型不能用数值来解决，但由此产生的具有有限数量未知数的`discrete model`可以用数值方法来解决。这里要提到的是这个步骤中涉及的近似。这个过程将`discretization error`引入到解决方案中，这在很大程度上取决于离散化的质量和使用的方法。

\textbf{Solution}：求解`discrete model`，并获得阻尼和其他相关结果。这一步引入了来自使用算法的不精确性、机器的数字精度或其他来源的`solution error`。

图[3.1]显示了这个总体方案。这里的一个重要观察是不同概念引入的不同错误和近似的存在。这些误差的积累会影响到这些方法得到的结果的有效性。由于这个原因，每个步骤的有效性和减少每个过程中的误差是使用数值方法的主要挑战之一。

\subsubsection{Idealization} 

理想化是某些物理现象的`mathematical modeling`。这一步的主要任务是为一个给定的物理问题找到一个合适的数学模型。

数学模型通常基于不同的假设。这种依赖性使它们对这些假设正确或接近现实的问题有用。由于这个原因，找到一个好的数学模型不仅需要对问题有很好的了解，还需要对模型的假设有很好的了解。

例如，在解决一个流体问题时，理想化包括为问题中的某种流体找到最佳的流体模型。在这种情况下，主要的问题是。这个流体是牛顿式的吗？它是可压缩的还是不可压缩的？它是层流吗？根据这些条件，一个模型可以被认为比其他模型更适合于某个问题。然而，所选择的模型仍然可能给我们的解决方案带来某些建模错误。

有不同形式的数学模型。一些常见的是。

\textbf{Strong Form} 将数学模型定义为一个普通或偏微分方程系统和一些相应的边界条件。

\textbf{Weak Form} 它使用加权残差近似法，以特定的修正形式表达数学方程。

\textbf{Variational Form} 在这种形式下，数学模型被表述为一个函数，其静止条件产生了弱形式。

\textbf{Strong Form} 如前所述，强形式将数学模型定义为一个普通或偏微分方程系统和一些相应的边界条件。考虑到图[3.2]所示的具有边界$\Gamma$的域$\Omega$，这种形式通过域和边界上的方程组定义了模型，如下所示。

\begin{equation}
\begin{cases}\mathcal{L}(u(x))=p & x \in \Omega \\ \mathcal{S}(u(x))=q & x \in \Gamma\end{cases} 
\label{3.1}
\end{equation}

其中$u(x)$是未知数，$\mathcal{L}$是在域上应用的算子$\Omega$，$\mathcal{S}$代表在边界上应用的算子$\Gamma$ 。第一个方程代表域上的治理方程，第二个方程代表这个问题的边界条件。例如，图[3.3]域上的热问题可以用下面的强形式来建模。

\begin{equation}
\begin{cases}\nabla^{T} \mathbf{k} \nabla \theta(x)=Q & x \in \Omega \\ \theta(x)=\theta_{\Gamma} & x \in \Gamma_{\theta} \\ q(x)=q_{\Gamma} & x \in \Gamma_{q}\end{cases}
\label{3.2}
\end{equation}

其中$\theta(x)$是温度，$\Omega, q$是边界通量，$Q$是内部热源，$\Gamma_{\theta}$是固定温度的边界$\theta_{\Gamma}$ ，$\Gamma_{q}$是固定通量的边界$q_{\Gamma}$ 。

\textbf{Weak Form} 

设$V$是一个Banach空间，考虑寻找方程$u \in V$的解的问题

\begin{equation}
\mathcal{L}(u)=p \quad u \in V
\label{3.3}
\end{equation}


这个问题等价于在满足任意 $v \in V$ 下， 找到方程的解$u \in V$。 

\begin{equation}
(\mathcal{L}(u), v)=(p, v) \quad u \in V, \forall v \in V
\label{3.4}
\end{equation}


这被称为问题的`weak formulation`。弱形式定义了使用强形式的`weak formulation`的数学模型。现在使用以下标量乘积。

\begin{equation}
(u, v)=\int_{\Omega} u v d \Omega
\label{3.5}
\end{equation}

结果是`integral form`，它是模型的加权积分表示。

\begin{equation}
\int_{\Omega} \mathcal{L}(u) v d \Omega=\int_{\Omega} p v d \Omega \quad u \in V, \forall v \in V
\label{3.6}
\end{equation}

这种表述也可以应用于涉及边界条件。例如，由方程 \ref{3.1} 表示的相同问题可以重写如下:

\begin{equation}
\int_{\Omega} r(u) w d \Omega+\int_{\Gamma} \bar{r}(u) \bar{w} d \Gamma=0 \quad u \in V, \forall w, \bar{w} \in V
\label{3.7}
\end{equation}

其中$r$和$\bar{r}$是分别定义在域和边界上的残差函数。

\begin{align}
&r(u)=\mathcal{L}(u)-p \label{3.8} \\
&\bar{r}(u)=S(u)-q \label{3.9}
\end{align}

以及$w$和$\bar{w}$是域和边界上的任意加权函数。通常情况下，为了减少方程中导数的最大阶数并通过对加权函数应用一些导数来平衡它，使用部分积分是很方便的。对方程 \ref{3.7} 进行分项积分可以得到。

\begin{equation}
\int_{\Omega} A(u) B(w) d \Omega+\int_{\Gamma} C(u) D(\bar{w}) d \Gamma=0 \quad u \in V, \forall w, \bar{w} \in V
\label{3.10}
\end{equation}

减少$A$和$C$中的导数顺序，相对于$r$和$\bar{r}$，允许在选择$u$函数时降低连续性的顺序要求。然而，现在对$w$和$\bar{w}$更高的连续性是必要的。

\textbf{Variational Form} 

变分形式通常来自问题的一些基本量，如质量、动量或能量，其静止状态是令人感兴趣的。这种形式通过以下形式的函数定义了数学模型。

\begin{equation}
\Pi(u)=\int_{\Omega} F(u) d \Omega
\label{3.11}
\end{equation}

这个量的静止状态是必需的，因此，它的变化要等于零，这就产生了下面的方程式。

\begin{equation}
\delta \Pi(u)=\int_{\Omega} \delta(F(u)) d \Omega=0
\label{3.12}
\end{equation}







从守恒定律推导出变异方程对科学家来说是有吸引力的，因为它呈现了问题的相同基本特征。最后必须提到的是，弱形式也可以从变分形式的静止状态导出。

\subsubsection{Discretization} 

数值分析的第一步是定义一个与实际物理问题相对应的数学或连续模型。在实践中，除了某些领域和条件外，连续模型不能用分析法解决，这就是数值解决的原因。连续模型有无限多的未知数，对应于域和边界的点，不能直接用数值方法解决。因此，第二步是必要的，以便将连续模型转换为具有有限数量未知数的离散模型，可以用数值方法求解。

有几种方法可以进行这种转换，从而产生不同的数值方法。适当的离散化方法不仅取决于问题的类型，也取决于描述它的数学模型的类型。以下是对不同模型的适当离散化的简要描述。

\textbf{Discretization of the Strong Form} 

强形式的离散化通常使用 `finite difference method` 进行。这里的想法来自于用差分代替导数的数值计算。例如，函数$f(x)$的一阶导数可以改变为其离散形式，如下所示。

\begin{equation}
\frac{d f(x)}{d x} \approx \Delta_{h}^{1}(f, x)=\frac{1}{h}(f(x+h)-f(x))
\label{3.13}
\end{equation}

其中离散化参数$h$是网格点的距离。这种方法也可用于计算函数的高阶导数。

\begin{equation}
\frac{d^{n} f(x)}{d x^{n}} \approx \Delta_{h}^{n}(f, x)=\sum_{i=0}^{n}(-1)^{n-k}\left(\frac{1}{h}\right)^{n}\left(\begin{array}{c}
n \\
i
\end{array}\right) f(x+i h)
\label{3.14}
\end{equation}

离散化只是域上的一个笛卡尔网格。图[3.4]显示了一个二维空间中的网格样本。

这种方法已经在许多领域得到了实际应用，并在许多应用中得到了实现。它的方法很简单，也很容易编程。这些使得它成为数值分析中最受欢迎的方法之一。然而，这种方法也有其不足之处。 首先，它对规则域工作得很好，但对任意的几何形状和边界条件就会遇到困难。例如，图[3.5]（a）的不规则域可以用图[3.5]（b）所示的离散域来近似。可以很容易地看到，这种离散化改变了任意几何的域边界。



另一个缺点是它的近似解，只能在网格点得到，因此没有提供网格内其他点的信息。

\textbf{Discretization of the Weak Form} 

连续数学模型的离散化包括将工作空间转换为一些选定的离散空间。考虑到连续空间中的以下弱形式 $V$ 。

\begin{equation}
(\mathcal{L}(u), v)=(p, v) \quad u \in V, \forall v \in V
\label{3.15}
\end{equation}

这个形式可以被转换到离散空间$V_{h}$，以近似解$u_{h} \in V$ 。

\begin{equation}
\left(\mathcal{L}\left(u_{h}\right), v_{h}\right)=\left(p, v_{h}\right) \quad u_{h} \in V, \forall v_{h} \in V
\label{3.16}
\end{equation}

如前所述，数学模型的弱形式可以用加权积分表示为。

\begin{equation}
\int_{\Omega} r(u) w d \Omega+\int_{\Gamma} \bar{r}(u) \bar{w} d \Gamma=0 \quad u \in V, \forall w, \bar{w} \in V
\label{3.17}
\end{equation}

其中$r$和$\bar{r}$是分别定义在域和边界上的残差函数。 $w$和$\bar{w}$是域和边界上的任意加权函数。这里选择一个离散空间$V_{h}$作为我们的工作空间，结果是`discrete model` 。

\begin{equation}
\int_{\Omega} r\left(u_{h}\right) w_{h} d \Omega+\int_{\Gamma} \bar{r}\left(u_{h}\right) \bar{w}_{h} d \Gamma=0 \quad u_{h} \in V_{h}, \forall w_{h}, \bar{w}_{h} \in V_{h}
\label{3.18}
\end{equation}

其中$r$和$\bar{r}$是残差函数。方程 \ref{3.18} 是残差的加权积分。所以这类近似方法被称为加权残差法。一些众所周知的方法如有限`Element Method`（FEM）、`Finite Volume`（FV）和`Least squares fitting`都是这种方法的子类。通常选择一个由已知`trial functions`$N_{i}$的集合组成的离散空间$V_{h}$，并按以下方式定义离散解。

\begin{equation}
u_{h} \approx \sum_{j=1}^{n} a_{j} N_{j}
\label{3.19}
\end{equation}

其中$a_{j}$是未知系数，$N_{j}$是已知试算函数，$n$是未知数的数量。将其代入方程 \ref{3.18} 的结果。

\begin{equation}
\int_{\Omega} r\left(\sum_{j=1}^{n} a_{j} N_{j}\right) w_{h} d \Omega+\int_{\Gamma} \bar{r}\left(\sum_{j=1}^{n} a_{j} N_{j}\right) \bar{w}_{h} d \Gamma=0 \quad \forall w_{h}, \bar{w}_{h} \in V_{h}
\label{3.20}
\end{equation}

与

\begin{equation}
w_{h}=\sum_{i=1}^{n} \alpha_{i} w_{i} \quad, \quad \bar{w}_{h}=\sum_{i=1}^{n} \alpha_{i} \bar{w}_{i}
\label{3.21}
\end{equation}

其中$\alpha_{i}$为任意系数，$w_{i}$和$\bar{w}_{i}$为任意函数，$n$为未知数。展开方程 \ref{3.20} 可以得到。

\begin{equation}
\sum_{i=1}^{n} \alpha_{i}\left[\int_{\Omega} r\left(\sum_{j=1}^{n} a_{j} N_{j}\right) w_{i} d \Omega+\int_{\Gamma} \bar{r}\left(\sum_{j=1}^{n} a_{j} N_{j}\right) \bar{w}_{i} d \Gamma\right]=0 \quad \forall \alpha_{i}, w_{i}, \bar{w}_{i} \in V_{h}
\label{3.22}
\end{equation}

由于$\alpha_{i}$是任意的，为了满足方程，上述和的所有分量必须为零。这导致了以下的方程组。

\begin{equation}
\int_{\Omega} r\left(\sum_{j=1}^{n} a_{j} N_{j}\right) w_{i} d \Omega+\int_{\Gamma} \bar{r}\left(\sum_{j=1}^{n} a_{j} N_{j}\right) \bar{w}_{i} d \Gamma=0 \quad \forall w_{i}, \bar{w}_{i} \in V_{h} \quad, \quad i=1,2,3, \ldots, n
\label{3.23}
\end{equation}

有几组的函数可以作为加权函数使用。下面是一些常见选择的`list`。

搭配法 使用`Dirac`的`delta`$\delta_{i}$作为加权函数。

\begin{equation}
w_{i}=\delta_{i} \quad, \quad \bar{w}_{i}=\delta_{i}
\label{3.24}
\end{equation}

其中$\delta_{i}$是一个函数，以便于。

\begin{equation}
\int_{\Omega} f \delta_{i} d \Omega=f_{i}
\label{3.25}
\end{equation}

将这个加权函数代入我们的参考方程 \ref{3.23}，得出以下`discrete model` 。

\begin{equation}
r_{i}\left(\sum_{j=1}^{n} a_{j} N_{j}\right)+\bar{r}_{i}\left(\sum_{j=1}^{n} a_{j} N_{j}\right)=0 \quad, \quad i=1,2,3, \ldots, n
\label{3.26}
\end{equation}

这种方法满足了方程在拼合点集合中的要求，并给出了与`finite difference method`得到的方程相似的的离散方程组。

子域法 它是前一种方法的延伸。它使用一个加权函数$w_{i}$，该函数在子域$\Omega_{i}$中是相同的，在其他地方是零。

\begin{equation}
w_{i}=\left\{\begin{array}{ll}
I & x \in \Omega_{i} \\
0 & x \notin \Omega_{i}
\end{array} \quad, \quad \bar{w}_{i}= \begin{cases}I & x \in \Gamma_{i} \\
0 & x \notin \Gamma_{i}\end{cases}\right.
\label{3.27}
\end{equation}

`discrete model`可以通过将这个加权函数代入方程3.23的一般弱形式得到。

\begin{equation}
\int_{\Omega_{i}} r\left(\sum_{j=1}^{n} a_{j} N_{j}\right) d \Omega_{i}+\int_{\Gamma_{i}} \bar{r}\left(\sum_{j=1}^{n} a_{j} N_{j}\right) d \Gamma_{i}=0 \quad, \quad i=1,2,3, \ldots, n
\label{3.28}
\end{equation}

这种方法在每个子域中提供了一个统一的近似值，并建立了一种将域划分为子域的方法来解决问题。

最小平方法 这种方法使用应用于试验函数的治理算子作为其加权函数。

\begin{equation}
w_{i}=\delta \mathcal{L}\left(N_{i}\right) \quad, \quad \bar{w}_{i}=\delta S\left(N_{i}\right)
\label{3.29}
\end{equation}

下面是将上述加权函数代入方程3.23的结果`discrete model`。

\begin{equation}
\int_{\Omega} r\left(\sum_{j=1}^{n} a_{j} N_{j}\right) \mathcal{L}\left(N_{i}\right) d \Omega+\int_{\Gamma} \bar{r}\left(\sum_{j=1}^{n} a_{j} N_{j}\right) S\left(N_{i}\right) d \Gamma=0 \quad, \quad i=1,2,3, \ldots, n
\label{3.30}
\end{equation}

我们可以验证，所得到的方程等同于在域上最小化全局残差$\mathcal{R}$的平方。

\begin{equation}
\delta \mathcal{R}=0
\label{3.31}
\end{equation}

其中,

\begin{equation}
\mathcal{R}=\int_{\Omega} r^{2}\left(u_{h}\right) d \Omega+\int_{\Gamma} \bar{r}^{2}\left(u_{h}\right) d \Gamma=0
\label{3.32}
\end{equation}

Galerkin方法使用试验函数作为加权函数。

\begin{equation}
w_{i}=N_{i} \quad, \quad \bar{w}_{i}=N_{i}
\label{3.33}
\end{equation}

将方程 \ref{3.33} 代入方程 \ref{3.23}，得到以下Galerkin discrete model 。

\begin{equation}
\int_{\Omega} r\left(\sum_{j=1}^{n} a_{j} N_{j}\right) N_{i} d \Omega+\int_{\Gamma} \bar{r}\left(\sum_{j=1}^{n} a_{j} N_{j}\right) N_{i} d \Gamma=0 \quad, \quad i=1,2,3, \ldots, n
\label{3.34}
\end{equation}

这种方法通常能改善求解过程，因为它经常（但不总是）导致对称矩阵和其他一些有用的特征，这使得它成为最受欢迎的方法和有限元求解的通常基础。

\textbf{Discretization of the Variational Form} 

`Rayleigh-Ritz`方法是连续模型的变量形式的经典离散化方法。它也是第一个试用函数的方法。其思想是通过$\tilde{u}$来近似解$u$，该方法由以下Trial函数集合的定义。

\begin{equation}
\tilde{u}=\sum_{i=1}^{n} \alpha_{i} N_{i}
\label{3.35}
\end{equation}

其中$\alpha_{i}$为未知系数，$N_{i}$为已知试算函数，$n$为未知数。现在考虑以下连续模型的变化形式。

\begin{equation}
\delta \Pi(u)=\int_{\Omega} \delta(F(u)) d \Omega=0
\label{3.36}
\end{equation}

将方程 \ref{3.35} 的试验函数展开插入方程 \ref{3.36} 中，得到。

\begin{equation}
\delta \Pi(\tilde{u})=\sum_{i=1}^{n} \frac{\partial \Pi}{\partial \alpha_{i}} \delta \alpha_{i}=0
\label{3.37}
\end{equation}

由于这个方程对任何变化都必须是真的$\delta \alpha$，它的所有分量必须等于零。这就形成了以下的方程组。

\begin{equation}
\begin{aligned}
&\frac{\partial \Pi(\tilde{u})}{\partial \alpha_{1}}=\int_{\Omega} \frac{\partial(F(\tilde{u}))}{\partial \alpha_{1}} d \Omega=0 \\
&\frac{\partial \Pi(\tilde{u})}{\partial \alpha_{2}}=\int_{\Omega} \frac{\partial(F(\tilde{u}))}{\partial \alpha_{2}} d \Omega=0 \\
&\vdots \\
&\frac{\partial \Pi(\tilde{u})}{\partial \alpha_{n}}=\int_{\Omega} \frac{\partial(F(\tilde{u}))}{\partial \alpha_{n}} d \Omega=0
\end{aligned}
\label{3.38}
\end{equation}

\textbf{Mixed Discretization} 

有时，为了描述复杂的现象，或者只是为了简化一些近似值，而在问题的其他方面保持更详细的方法，混合两种或更多类型的离散化是有用的。混合不同形式的一个典型案例是时间相关问题的建模，可以在时间上使用有限差分离析，而在空间上使用加权残差分析。

\subsubsection{Solution} 

数值方法的最后一步是求解。这一步包括使用适当的算法来解决`discrete model`，以找到主要的未知数，同时计算问题的其他额外未知数。这个过程包括。

\textbf{Calculating Components} 计算`discrete model`的所有组成部分（如`finite difference method`中的导数，加权残差法或变分法中的积分，等等）。

\textbf{Creating the Global System} `discrete model`的组成部分被放在一起，以建立代表`discrete model`的全局方程组。

\textbf{Solving the Global System} 全局系统必须被解决以计算问题的未知数。对于一些模型，这导致了一个线性方程组，可以用线性求解器来解决。一些算法创建了一个对角线全局系统，使求解部分完全变得微不足道。

\textbf{Calculating Additional Results} 在许多问题中，不仅主要的未知数，即结构问题中的位移，是感兴趣的，而且一些额外的结果，如结构问题中的应力和应变，也必须被计算。

\textbf{Iterating} 在许多算法中，还需要一些迭代来确定未知数或计算不同的未知数集。解决非线性问题，计算与时间有关的未知数，以及优化问题，都是需要迭代的算法的例子。

\subsection{Finite Element Method} 

在上一节中，对数值方法进行了简要介绍。由于这项工作是以有限元方法为基础的，因此对这种方法及其基本步骤进行了简要描述。

有限元方法（FEM）一般以问题的`integral form`为基础，使用分片多项式作为其试验函数。导致有限元法的公式有很多，但最常用的是Galerkin方法，这里用来描述有限元法及其基本步骤。

\subsubsection{Discretization} 

考虑到一个连续问题。

\begin{equation}
\begin{cases}\mathcal{L}(u(x))=p & x \in \Omega \\ \mathcal{S}(u(x))=q & x \in \Gamma\end{cases}
\label{3.39}
\end{equation}

和它的`integral form`如下:

\begin{equation}
\int_{\Omega} r(u) w d \Omega+\int_{\Gamma} \bar{r}(u) \bar{w} d \Gamma=0 \quad u \in V, \forall w, \bar{w} \in V
\label{3.40}
\end{equation}

其中$r$和$\bar{r}$是分别定义在域和边界上的残差函数。


\begin{align}
&r(u)=\mathcal{L}(u)-p \label{3.41} \\
&\bar{r}(u)=\mathcal{S}(u)-q \label{3.42}
\end{align}

将离散有限元空间$V_{h}$定义为多项式函数的组成$N_{i}$ 。

\begin{equation}
x=\sum_{i=1}^{n} \alpha_{i}^{x} N_{i}
\label{3.43}
\end{equation}

并将方程 \ref{3.40} 转换到这个空间，结果是:

\begin{equation}
\int_{\Omega} r\left(u_{h}\right) w_{h} d \Omega+\int_{\Gamma} \bar{r}\left(u_{h}\right) \bar{w}_{h} d \Gamma=0 \quad u_{h} \in V_{h}, \forall w_{h}, \bar{w}_{h} \in V_{h}
\label{3.44}
\end{equation}

其中,

\begin{align}
u_{h} &=\sum_{i=1}^{n} \alpha_{i} N_{i}  \label{3.45}
\\
w &=\sum_{i=1}^{n} \beta_{i} N_{i}  \label{3.46}
\\
\bar{w} &=\sum_{i=1}^{n} \beta_{i} N_{i} \label{3.47}
\end{align}

用这些定义展开方程 \ref{3.44} 的结果是：

\begin{equation}
\int_{\Omega} r\left(\sum_{j=1}^{n} \alpha_{j} N_{j}\right) \sum_{i=1}^{n} \beta_{i} N_{i} d \Omega+\int_{\Gamma} \bar{r}\left(\sum_{j=1}^{n} \alpha_{j} N_{j}\right) \sum_{i=1}^{n} \beta_{i} N_{i} d \Gamma=0 \quad \alpha, N \in V_{h} \quad, \quad \forall \beta \in V_{h}
\label{3.48}
\end{equation}




通过从积分中取出$\beta_{i}$，可以得到以下替代形式。

\begin{equation}
\sum_{i=1}^{n} \beta_{i}\left[\int_{\Omega} r\left(\sum_{j=1}^{n} \alpha_{j} N_{j}\right) N_{i} d \Omega+\int_{\Gamma} \bar{r}\left(\sum_{j=1}^{n} \alpha_{j} N_{j}\right) N_{i} d \Gamma\right]=0 \quad \alpha, N \in V_{h} \quad, \quad \forall \beta \in V_{h}
\label{3.49}
\end{equation}

由于$\beta_{i}$是任意的，为了满足方程，上述之和的所有分量必须为零。这就产生了以下的方程组。

\begin{equation}
\int_{\Omega} r\left(\sum_{j=1}^{n} \alpha_{j} N_{j}\right) N_{i} d \Omega+\int_{\Gamma} \bar{r}\left(\sum_{j=1}^{n} \alpha_{j} N_{j}\right) N_{i} d \Gamma=0 \quad \alpha, N \in V_{h} \quad, \quad i=1,2,3, \ldots, n
\label{3.50}
\end{equation}

这里的一个观察是方程 \ref{3.50} 中的离散形式与第3.1.3节中通过Galerkin方法得到的方程 \ref{3.34} 中的形式是等同的。

\textbf{Node and Degree of Freedom} 

在上一节中，解释了将方程 \ref{3.40} 中的连续`integral form`转换为方程 \ref{3.50} 的离散形式的一般过程。在有限元方法中，每个未知值被称为`degree of freedom`（自由度），它被认为是在称为`node`的域点上的有限元解$u_{h}$ 。

\begin{equation}
\alpha_{i}=a_{i}
\label{3.51}
\end{equation}

其中$a_{i}$是在节点$i$的近似解$u_{h}$。利用这一假设，方程组 \ref{3.50} 可以改写为：

\begin{equation}
\int_{\Omega} r\left(\sum_{j=1}^{n} a_{j} N_{j}\right) N_{i} d \Omega+\int_{\Gamma} \bar{r}\left(\sum_{j=1}^{n} a_{j} N_{j}\right) N_{i} d \Gamma=0 \quad a, N \in V_{h} \quad, \quad i=1,2,3, \ldots, n
\label{3.52}
\end{equation}

这组方程组可以通过求解直接得到域的每个节点的未知数。将方程 \ref{3.51} 代入 \ref{3.45} 的结果。

\begin{equation}
u_{h}=\sum_{i=1}^{n} a_{i} N_{i}
\label{3.53}
\end{equation}

其中将域上的近似解$u_{h}$与解决方程3.52的前一组得到的节点值联系起来。这样，不仅可以在所有节点上获得近似解，而且可以在域的任何其他点上获得近似解。

\textbf{Shape Functions and Elements} 

回到方程 \ref{3.52} ，一组的试验函数$N_{i}$对于定义问题是必要的。在有限元方法中，这些函数被称为`shape functions` 。`shape functions`的正确定义在正确逼近解和它的许多重要特性方面起着重要作用。

上一节介绍的离散化将方程 \ref{3.40} 中无限多的未知数减少到方程组 \ref{3.52} 中的有限数$n$。这是使模型在数值上可解的一大步，但对于在实践中解决它来说仍然是不完整的。问题来自于方程组组中的每个方程 \ref{3.52} 都涉及到对域的完整积分和方程中未知数之间的完整关系，这使得求解的成本非常高。为了避免这些问题，让我们把域$\Omega$分成几个子域$\Omega^{e}$，如下:

\begin{equation}
\Omega^{1} \cup \Omega^{2} \cup \ldots \cup \Omega^{e} \cup \ldots \cup \Omega^{m}=\Omega \quad, \quad \Omega^{1} \cap \Omega^{2} \cap \ldots \cap \Omega^{e} \cap \ldots \cap \Omega^{m}=\emptyset
\label{3.54}
\end{equation}

其中每个分区$\Omega^{e}$被称为一个元素。将方程 \ref{3.53} 中的积分相除，得到。

\begin{equation}
\sum_{e=1}^{m}\left[\int_{\Omega^{e}} r\left(\sum_{j=1}^{n} a_{j} N_{j}\right) N_{i} d \Omega^{e}+\int_{\Gamma^{e}} \bar{r}\left(\sum_{j=1}^{n} a_{j} N_{j}\right) N_{i} d \Gamma^{e}\right]=0 \quad i=1,2,3, \ldots, n
\label{3.55}
\end{equation}

其中$\Gamma^{e}$是边界$\Gamma$中与元素$\Omega_{e}$相关的部分，如图[^3.6]所示。现在让我们定义形状函数$N_{i}$如下:

\begin{equation}
N_{i}= \begin{cases}N_{i}^{e} & x \in \Omega^{e} \cup \Gamma^{e} \\ 0 & x \notin \Omega^{e} \cup \Gamma^{e}\end{cases}
\label{3.56}
\end{equation}

其中$\Omega^{e}$是一个叫做`element`的域的分区。将这个形状函数代入方程 \ref{3.52}，得到以下方程。

\begin{equation}
\sum_{e=1}^{m_{i}}\left[\int_{\Omega^{e}} r\left(\sum_{j=1}^{m_{i}} a_{j} N_{j}^{e}\right) N_{i}^{e} d \Omega^{e}+\int_{\Gamma^{e}} \bar{r}\left(\sum_{j=1}^{m_{i}} a_{j} N_{j}^{e}\right) N_{i}^{e} d \Gamma^{e}\right]=0 \quad i=1,2,3, \ldots, n
\label{3.57}
\end{equation}





其中 $m_{i}$ 是包含节点 $i$ 的元素数量。通过这种方式，未知数之间的关系被简化为邻居之间的关系，在每个方程中，必须只对一些元素进行积分。

\textbf{Boundary Conditions} 

考虑到以下问题。

\begin{equation}
\begin{cases}\mathcal{L}(u(x))=p & x \in \Omega \\ \mathcal{S}(u(x))=q & x \in \Gamma\end{cases}
\label{3.58}
\end{equation}

并假定$\mathcal{L}$最多包含$m$三阶导数。这种问题的边界条件可以分为两类：基本边界条件和自然边界条件。

基本边界条件$\mathcal{S}_{D}$是指包含小于$m-1$阶导数的条件。这些条件也被称为`Dirichlet`条件（以Peter Dirichlet命名）。例如，结构问题中的规定位移就是一个`Dirichlet`条件。

其余的边界条件被认为是自然边界条件 $\mathcal{S}_{N}$ 。这些条件也被称为`Neumann `边界条件（以卡尔`Neumann `命名）。例如，在一个结构问题中，边界牵引力是问题的`Neumann `条件。

对方程 \ref{3.58} 应用这种除法，结果是:

\begin{equation}
\begin{cases}\mathcal{L}(u(x))=p & x \in \Omega \\ \mathcal{S}_{D}(u(x))=q_{D} & x \in \Gamma_{D} \\ \mathcal{S}_{N}(u(x))=q_{N} & x \in \Gamma_{N}\end{cases}
\label{3.59}
\end{equation}

其中$\mathcal{S}_{D}$是应用于边界$\Gamma_{D}$的`Dirichlet`条件，$\mathcal{S}_{N}$是应用于边界$\Gamma_{N}$的`Neumann `条件，如图3.7所见。将方程 \ref{3.59} 转换为其`integral form`的结果。

\begin{equation}
\int_{\Omega} r(u) w d \Omega+\int_{\Gamma_{D}} \bar{r}_{D}(u) \bar{w} d \Gamma_{D}+\int_{\Gamma_{N}} \bar{r}_{N}(u) \bar{w} d \Gamma_{N}=0 \quad u \in V, \forall w, \bar{w} \in V
\label{3.60}
\end{equation}

其中,






\begin{align}
r(u) &=\mathcal{L}(u)-p  \label{3.61}\\
\bar{r}_{D}(u) &=\mathcal{S}_{D}(u)-q_{D}  \label{3.61}\\
\bar{r}_{N}(u) &=\mathcal{S}_{N}(u)-q_{N} \label{3.62}
\end{align}


如果解$u$的选择是`restricted`满足$\Gamma_{D}$上的`Dirichlet`条件的函数，通过限制`Dirichlet`边界$\Gamma_{D}$上的积分可以省略在$\Gamma_{D}$上为零的函数。使用这些限制的结果是:

\begin{align}
\int_{\Omega} r(u) w d \Omega+\int_{\Gamma_{N}} \bar{r}_{N}(u) \bar{w} d \Gamma_{N} & =0 & u \in V, \forall w, \bar{w} \in V  \label{3.64} \\
u & =\bar{u} & x \in \Gamma_{D}  \label{3.65}
\end{align}

其中$\bar{u}$是`Dirichlet`边界上的解。使用之前描述的相同过程将上述模型转换为其离散形式，得到以下离散方程。

\begin{align}
\int_{\Omega} r\left(\sum_{j=1}^{n} a_{j} N_{j}\right) N_{i} d \Omega+\int_{\Gamma_{N}} \bar{r}_{N}\left(\sum_{j=1}^{n} a_{j} N_{j}\right) N_{i} d \Gamma_{N}=0 \quad a, N \in V_{h} \quad, \quad i=1,2, \ldots, n  \label{3.66}\\
u=\bar{u} \quad x \in \Gamma_{D}  \label{3.67}
\end{align}

\subsubsection{Solution} 

本节将描述一般有限元求解过程的全局流程，并解释一些用于提高实践效率的技术。

\textbf{Calculating Components} 

将基本条件应用于方程 \ref{3.57} 的结果是:


\begin{align}
\sum_{e=1}^{m_{i}}\left[\int_{\Omega^{e}} r\left(\sum_{j=1}^{m_{i}} a_{j} N_{j}^{e}\right) N_{i}^{e} d \Omega^{e}+\int_{\Gamma_{N}^{e}} \bar{r}\left(\sum_{j=1}^{m_{i}} a_{j} N_{j}^{e}\right) N_{i}^{e} d \Gamma_{N}^{e}\right] &=0 \quad i=1,2, \ldots, n    \label{3.68}\\
u &=\bar{u} x \in \Gamma_{D}   \label{3.69}
\end{align} 


如果微分方程是线性的，我们可以把上面的方程写成如下:

\begin{equation}
\mathbf{K a}+\mathbf{f}=0   \label{3.70}
\end{equation}

其中,


\begin{align}
\mathbf{K}_{i j} &=\sum_{e=1}^{m} \mathbf{K}_{i j}^{e}    \label{3.71}\\
\mathbf{f}_{i} &=\sum_{e=1}^{m} \mathbf{f}_{i}^{e}  \label{3.72}
\end{align}

这一步包括计算`shape functions`和它们在每个元素中的导数，然后对每个元素进行积分。

这里通常的技术是在元素的局部坐标中计算这些分量，然后将结果转换为全局坐标。通常，`shape functions`是以局部坐标定义的，它们的值和相对于局部坐标的梯度是已知的。然而，元素矩阵包含`shape functions`相对于全局坐标的梯度。这些梯度可以用本地梯度和一些矩阵$\mathbf{J}$的逆值来计算，称为`jacobian matrix` 。考虑到全局坐标$x, y, z$和元素局部坐标$\xi, \eta, \zeta$，可以看出`shape functions`相对于局部坐标的梯度可以用全局坐标写成如下:

\begin{equation}
\left[\begin{array}{l}
\frac{\partial N_{i}}{\partial \xi} \\
\frac{\partial N_{i}}{\partial \eta} \\
\frac{\partial N_{i}}{\partial \zeta}
\end{array}\right]=\left[\begin{array}{ccc}
\frac{\partial x}{\partial \xi} & \frac{\partial y}{\partial \xi} & \frac{\partial z}{\partial \xi} \\
\frac{\partial x}{\partial \eta} & \frac{\partial y}{\partial \eta} & \frac{\partial z}{\partial \eta} \\
\frac{\partial x}{\partial \zeta} & \frac{\partial y}{\partial \zeta} & \frac{\partial z}{\partial \zeta}
\end{array}\right]\left[\begin{array}{l}
\frac{\partial N_{i}}{\partial x} \\
\frac{\partial N_{i}}{\partial y} \\
\frac{\partial N_{i}}{\partial z}
\end{array}\right]=\mathbf{J}\left[\begin{array}{c}
\frac{\partial N_{i}}{\partial x} \\
\frac{\partial N_{i}}{\partial y} \\
\frac{\partial N_{i}}{\partial z}
\end{array}\right]
\end{equation}

其中$\mathbf{J}$是`jacobian matrix` 。现在，`shape functions`相对于全局坐标的梯度可以被计算如下:

\begin{equation}
\left[\begin{array}{l}
\frac{\partial N_{i}}{\partial x} \\
\frac{\partial N_{i}}{\partial y} \\
\frac{\partial N_{i}}{\partial z}
\end{array}\right]=\mathbf{J}^{-1}\left[\begin{array}{l}
\frac{\partial N_{i}}{\partial \xi} \\
\frac{\partial N_{i}}{\partial \eta} \\
\frac{\partial N_{i}}{\partial \zeta}
\end{array}\right]
\end{equation}

在计算出`shape functions`相对于全局坐标的梯度后，我们可以对元素进行积分。这可以通过将积分域从全局坐标转换为局部坐标来完成，如下所示。

\begin{equation}
\int_{\Omega^{e}} f d \Omega^{e}=\int_{\Omega^{e}} f \operatorname{det} \mathbf{J} d \xi d \eta d \zeta
 \label{3.73}
\end{equation}

现在，所有的元素矩阵都可以用局部坐标计算，并转换为全局坐标。计算元素上的积分的通常方法是使用`Gaussian Quadrature`方法。这种方法将一个函数在域上的积分转换为某些样本点上的函数值的加权和，如下所示。

\begin{equation}
\int_{a}^{b} f(x) d x \approx \sum_{i=1}^{n} w_{i} f\left(x_{i}\right)
 \label{3.74}
\end{equation}







这种方法使用了近一半的样本点，以达到与其他经典四分法相同的精度水平。因此，它是FEM中计算元素积分的一种有效方法。这种方法以及其他一些积分方法将在后面的5.1节中解释。

\textbf{Creating the Global System} 

如前所述，对于线性微分方程，方程组 \ref{3.68} 可以写成全局方程组的形式。

\begin{equation}
\mathbf{K a}=\mathbf{f}
 \label{3.75}
\end{equation}

其中,


\begin{align}
\mathbf{K}_{i j} &=\sum_{e=1}^{m} \mathbf{K}_{i j}^{e}   \label{3.76}\\
\mathbf{f}_{i} &=\sum_{e=1}^{m} \mathbf{f}_{i}^{e}    \label{3.77}
\end{align}

然而上述方程中的和必须应用于相应的坐标。有了元素矩阵和向量$\mathbf{K}^{e}$和$\mathbf{f}^{e}$，把它们放在一起以建立全局方程组 \ref{3.75} 的程序称为`assembly`，包括找到全局方程组中每个元素成分的位置，并将其与位置中的值相加。

这个程序首先给所有的自由度分配一个连续的编号。有时将`restricted`自由度，即具有`Dirichlet`条件的自由度与其他自由度分开是很有用的。这可以在给自由度分配指数的时候很容易做到。之后，该程序逐个元素进行，并使用以下`assembly`操作符$\sqcup$将其本地矩阵和向量添加到全局方程系统中。

\begin{align}
\mathbf{K}_{i j} \bigcup_{\mathbf{I}^{e}} \mathbf{K}_{i j}^{e}=\mathbf{K}_{\mathbf{I}_{i} \mathbf{I}_{j}^{e}}+\mathbf{K}_{i j}^{e}   \label{3.78}\\
\mathbf{f}_{i} \bigcup_{\mathbf{I}^{e}}\mathbf{f}_{i}^{e}=\mathbf{f}_{\mathbf{I}_{i}^{e}}+\mathbf{f}_{i}^{e}  \label{3.79}
\end{align}


其中$I^{e}$是包含全局位置的`vector`，它是每一行或每一列对应的自由度的索引。例如，考虑图[3.8]的梁问题，有两个元素和以下元素矩阵和向量。

\begin{equation}
\mathbf{K}^{(1)}=\left[\begin{array}{cccc}
K_{11}^{(1)} & K_{12}^{(1)} & K_{13}^{(1)} & K_{14}^{(1)} \\
K_{21}^{(1)} & K_{22}^{(1)} & K_{23}^{(1)} & K_{24}^{(1)} \\
K_{31}^{(1)} & K_{32}^{(1)} & K_{33}^{(1)} & K_{34}^{(1)} \\
K_{41}^{(1)} & K_{42}^{(1)} & K_{43}^{(1)} & K_{44}^{(1)}
\end{array}\right] \quad, \quad \mathbf{f}^{(1)}=\left[\begin{array}{c}
f_{1}^{(1)} \\
f_{2}^{(1)} \\
f_{3}^{(1)} \\
f_{4}^{(1)}
\end{array}\right]
 \label{3.80}
\end{equation}

和

\begin{equation}
\mathbf{K}^{(2)}=\left[\begin{array}{cccc}
K_{11}^{(2)} & K_{12}^{(2)} & K_{13}^{(2)} & K_{14}^{(2)} \\
K_{21}^{(2)} & K_{22}^{(2)} & K_{23}^{(2)} & K_{24}^{(2)} \\
K_{31}^{(2)} & K_{32}^{(2)} & K_{33}^{(2)} & K_{34}^{(2)} \\
K_{41}^{(2)} & K_{42}^{(2)} & K_{43}^{(2)} & K_{44}^{(2)}
\end{array}\right] \quad, \quad \mathbf{f}^{(2)}=\left[\begin{array}{c}
f_{1}^{(2)} \\
f_{2}^{(2)} \\
f_{3}^{(2)} \\
f_{4}^{(2)}
\end{array}\right]
\label{3.81}
\end{equation}

给予斗室$a_{1}$到$a_{6}$的顺序索引，元素$e_{1}$和$e_{2}$的索引向量$I^{1}$和$I^{2}$将是：

\begin{equation}
I^{(1)}=\left[\begin{array}{l}
1 \\
2 \\
3 \\
4
\end{array}\right] \quad, \quad I^{(2)}=\left[\begin{array}{c}
3 \\
4 \\
5 \\
6
\end{array}\right]
\label{3.82}
\end{equation}

最后，使用上面的索引向量组装元素矩阵和向量，会得到以下系统。

\begin{equation}
\mathrm{Ka}=\mathrm{f}
\label{3.83}
\end{equation}

与

\begin{equation}
\mathbf{K}=\left[\begin{array}{cccccc}
K_{11}^{(1)} & K_{12}^{(1)} & K_{13}^{(1)} & K_{14}^{(1)} & 0 & 0 \\
K_{21}^{(1)} & K_{22}^{(1)} & K_{23}^{(1)} & K_{24}^{(1)} & 0 & 0 \\
K_{31}^{(1)} & K_{32}^{(1)} & K_{33}^{(1)}+K_{11}^{(2)} & K_{34}^{(1)}+K_{12}^{(2)} & K_{13}^{(2)} & K_{14}^{(2)} \\
K_{41}^{(1)} & K_{42}^{(1)} & K_{43}^{(1)}+K_{21}^{(2)} & K_{44}^{(1)}+K_{22}^{(2)} & K_{23}^{(2)} & K_{24}^{(2)} \\
0 & 0 & K_{31}^{(2)} & K_{32}^{(2)} & K_{33}^{(2)} & K_{34}^{(2)} \\
0 & 0 & K_{41}^{(2)} & K_{42}^{(2)} & K_{43}^{(2)} & K_{44}^{(2)}
\end{array}\right]
\label{3.84}
\end{equation}

和

\begin{equation}
\mathbf{f}=\left[\begin{array}{c}
f_{1}^{(1)} \\
f_{2}^{(1)} \\
f_{3}^{(1)}+f_{1}^{(2)} \\
f_{4}^{(1)}+f_{2}^{(2)} \\
f_{3}^{(2)} \\
f_{4}^{(2)}
\end{array}\right]
\label{3.85} 
\end{equation}

在建立全局方程组时要做的另一项工作是应用基本边界条件。这可以通过从全局矩阵和向量中消除对应于`restricted dof`的行和列，并将其相应的值应用到右手边来轻松完成。这个过程可以在没有`reordering`方程的情况下进行，但为了简化过程，将`restricted`方程与其他方程分开更为方便。考虑以下方程组，其中对应于`Dirichlet`自由度的部分与其他部分分开。

\begin{equation}
\left[\begin{array}{ll}
\mathbf{K}_{N N} & \mathbf{K}_{D N} \\
\mathbf{K}_{D N} & \mathbf{K}_{D D}
\end{array}\right]\left[\begin{array}{l}
\mathbf{a}_{N} \\
\mathbf{a}_{D}
\end{array}\right]=\left[\begin{array}{c}
\mathbf{f}_{N} \\
\mathbf{f}_{D}
\end{array}\right]
\label{3.86}
\end{equation}

其中$\mathbf{a}_{N}$是未知数，$\mathbf{a}_{D}$是已知的自由度，有`Dirichlet`边界条件和$\mathbf{f}_{D}$它们相应的边界未知数。让我们把上述系统分为两个`restricted`和非`restricted`部分，如下:

\begin{align}
&{\left[\mathbf{K}_{N N}\right]\left[\mathbf{a}_{N}\right]+\left[\mathbf{K}_{N D}\right]\left[\mathbf{a}_{D}\right]=\left[\mathbf{f}_{N}\right]}  \label{3.87}\\
&{\left[\mathbf{K}_{D N}\right]\left[\mathbf{a}_{N}\right]+\left[\mathbf{K}_{D D}\right]\left[\mathbf{a}_{D}\right]=\left[\mathbf{f}_{D}\right]}  \label{3.88}
\end{align}


知道了`Dirichlet`边界条件值$\mathbf{a}_{D}$，让我们把它们移到右手边。

\begin{equation}
\left[\mathbf{K}_{N N}\right]\left[\mathbf{a}_{N}\right]=\left[\mathbf{f}_{N}\right]-\left[\mathbf{K}_{N D}\right]\left[\mathbf{a}_{D}\right] \label{3.89}
\end{equation}

这个方程组可以被解决以得到未知数 $\mathbf{a}_{N}$ 。考虑到之前的图中梁的例子 \ref{3.8} 。其中，$a_{1}$ 和 $a_{5}$ 是 `restricted` 。为了对全局系统进行划分，让我们对自由度重新排序如下:

\begin{equation}
\tilde{\mathbf{a}}=\left\{a_{2}, a_{3}, a_{4}, a_{6}, a_{1}, a_{5}\right\}  \label{3.90}
\end{equation}

这导致了以下的索引向量。

\begin{equation}
I^{(1)}=\left[\begin{array}{l}
5 \\
1 \\
2 \\
3
\end{array}\right] \quad, \quad I^{(2)}=\left[\begin{array}{l}
2 \\
3 \\
6 \\
4
\end{array}\right]
\label{3.91}
\end{equation}

现在让我们组装全局矩阵 $\mathbf{K}$ 。

\begin{equation}
\mathbf{K}=\left[\begin{array}{cccccc}
K_{22}^{(1)} & K_{23}^{(1)} & K_{24}^{(1)} & 0 & K_{21}^{(1)} & 0 \\
K_{32}^{(1)} & K_{33}^{(1)}+K_{11}^{(2)} & K_{34}^{(1)}+K_{12}^{(2)} & K_{14}^{(2)} & K_{31}^{(1)} & K_{13}^{(2)} \\
K_{42}^{(1)} & K_{43}^{(1)}+K_{21}^{(2)} & K_{44}^{(1)}+K_{22}^{(2)} & K_{24}^{(2)} & K_{41}^{(1)} & K_{23}^{(2)} \\
0 & K_{41}^{(2)} & K_{42}^{(2)} & K_{44}^{(2)} & 0 & K_{43}^{(2)} \\
K_{12}^{(1)} & K_{13}^{(1)} & K_{14}^{(1)} & 0 & K_{11}^{(1)} & 0 \\
0 & K_{31}^{(2)} & K_{32}^{(2)} & K_{34}^{(2)} & 0 & K_{33}^{(2)}
\end{array}\right]
\label{3.92}
\end{equation}

和 向量 $\mathbf{f}$ 

\begin{equation}
\mathbf{f}=\left[\begin{array}{c}
f_{2}^{(1)} \\
f_{3}^{(1)}+f_{1}^{(2)} \\
f_{4}^{(1)}+f_{2}^{(2)} \\
f_{4}^{(2)} \\
f_{1}^{(1)} \\
f_{3}^{(2)}
\end{array}\right]
\label{3.93}
\end{equation}

最后用方程 \ref{3.89} 应用`Dirichlet`的边界条件。

\begin{equation}
\mathbf{K}_{\mathrm{NN}}=\left[\begin{array}{cccc}
K_{22}^{(1)} & K_{23}^{(1)} & K_{24}^{(1)} & 0 \\
K_{32}^{(1)} & K_{33}^{(1)}+K_{11}^{(2)} & K_{34}^{(1)}+K_{12}^{(2)} & K_{14}^{(2)} \\
K_{42}^{(1)} & K_{43}^{(1)}+K_{21}^{(2)} & K_{44}^{(1)}+K_{22}^{(2)} & K_{24}^{(2)} \\
0 & K_{41}^{(2)} & K_{42}^{(2)} & K_{44}^{(2)}
\end{array}\right]
\label{3.94}
\end{equation}

和

\begin{equation}
{\left[\mathbf{R}_{N}\right]=\left[\mathbf{f}_{N}\right]-\left[\mathbf{K}_{N D}\right]\left[\mathbf{a}_{D}\right]=\left[\begin{array}{c}
f_{2}^{(1)} \\
f_{3}^{(1)}+f_{1}^{(2)} \\
f_{4}^{(1)}+f_{2}^{(2)} \\
f_{4}^{(2)}
\end{array}\right]-\left[\begin{array}{cc}
K_{21}^{(1)} & 0 \\
K_{31}^{(1)} & K_{13}^{(2)} \\
K_{41}^{(1)} & K_{23}^{(2)} \\
0 & K_{43}^{(2)}
\end{array}\right]\left[\begin{array}{l}
a_{1} \\
a_{5}
\end{array}\right]} 
\label{3.95}
\end{equation}

\begin{equation}
\qquad\left[\begin{array}{c}
R_{1} \\
R_{2} \\
R_{3} \\
R_{4}
\end{array}\right]=\left[\begin{array}{c}
f_{2}^{(1)}-K_{21}^{(1)} a_{1} \\
f_{3}^{(1)}+f_{1}^{(2)}-K_{31}^{(1)} a_{1}-K_{13}^{(2)} a_{5} \\
f_{4}^{(1)}+f_{2}^{(2)}-K_{41}^{(1)} a_{1}-K_{23}^{(2)} a_{5} \\
f_{4}^{(2)}-K_{43}^{(2)} a_{5}
\end{array}\right] 
\label{3.96}
\end{equation}


结果是要解决以下方程。

\begin{equation}
\left[\begin{array}{cccc}
K_{22}^{(1)} & K_{23}^{(1)} & K_{24}^{(1)} & 0 \\
K_{32}^{(1)} & K_{33}^{(1)}+K_{11}^{(2)} & K_{34}^{(1)}+K_{12}^{(2)} & K_{14}^{(2)} \\
K_{42}^{(1)} & K_{43}^{(1)}+K_{21}^{(2)} & K_{44}^{(1)}+K_{22}^{(2)} & K_{24}^{(2)} \\
0 & K_{41}^{(2)} & K_{42}^{(2)} & K_{44}^{(2)}
\end{array}\right]\left[\begin{array}{c}
a_{2} \\
a_{3} \\
a_{4} \\
a_{6}
\end{array}\right]=\left[\begin{array}{c}
R_{1} \\
R_{2} \\
R_{3} \\
R_{4}
\end{array}\right]
\label{3.97}
\end{equation}

另一种应用`Dirichlet`条件的方法是使用`penalty method` 。这将一个大系数应用于对应于`restricted`自由度的对角线元素。这种方法更容易编程，但不如前一种方法稳健。问题是要找到正确的系数，因为一个高的数字可能导致系统条件不好，而一个低的系数则不太现实。

通过有限元得到的全局系统矩阵通常有很多零。在密集的矩阵结构中保存其所有的值，即存储矩阵的所有元素，这意味着由于零元素的存储而产生的大量内存开销。有几种替代性的结构可以保存矩阵中有用的部分用于求解。例如，带状矩阵结构在矩阵的对角线周围保留一个带子，并假定带子以外的所有元素都是零。还有一些稀疏的矩阵结构，如。 `compressed sparse row`（CSR），它将每一行的非零元素与它们相应的列数一起存储，或者压缩稀疏列（CSC），它是`compressed sparse row`的转置，是一种列的主要结构。另一种常见的结构是`symmetric matrix`结构，它利用矩阵的对称性来保存大约一半的元素，可以与稀疏结构相结合来存储矩阵中一半的非零元素。

\textbf{Solving the Global System} 

在上一节中，准备了全局方程组，应用基本条件使其可以被解决。这个方程组可以用传统的求解器进行求解。有两类求解器， `direct solvers` 和 `iterative solvers` 。

直接求解器试图通过使系数矩阵成为上三角、下三角、对角线，或者有时将其分解为上下层形式，并使用这种形式的矩阵计算未知数来解决方程。像`Gaussian elimination`[^81]、正面解法[^23]、一般矩阵的LU分解[^81]和对称矩阵的Cholesky[^81]等求解器就是这一类的例子。

迭代求解器从一些未知数的初始值开始，试图通过计算残差找到正确的解决方案，并在迭代中使其最小化。 `Conjugate gradient` $(C G)$ [^90] , `Conjugate gradient` (BCG) [^90] , `Generalized minimal residual method` (GMRES) [^90] 是这类求解器的例子。

直接求解器对于小的方程组来说是非常快的，而且对矩阵的条件依赖较少。唯一的例外是枢轴的存在，即对角线上的零，这需要特殊处理。这些求解器对于大系统来说非常慢，而操作的数量以$O\left(N^{3}\right)$的顺序增长，其中$N$是系统的大小。像`multi frontal solution`[^36]这样的算法被用来解决并行机器中的大系统，利用了几个处理器并行使用的优势。减少解题所需操作数量的方法是减少系统矩阵的带宽。

相反，迭代求解器高度依赖于系统的条件，这在很大程度上影响了其收敛性。通常，对于小型系统，直接求解器更快，而对于中大型系统，迭代求解器更适合，但仍取决于系统的条件。这些方法也比直接方法更容易实现和优化。

如前所述，直接求解器的求解成本高度依赖于系统矩阵的带宽。由于这个原因，推荐使用一个叫做`reordering`的程序来减少系统矩阵的带宽。这个程序包括改变矩阵的行和列的顺序，以便在求解前减少带宽，然后在求解后将结果重新排列。在实践中，这些算法可以应用于在自由度创建后以最佳方式重新编号，然后像往常一样求解系统。`Cuthill McKee` [^90]算法是这些算法的一个经典例子。有时，在使用迭代求解器之前会应用`reordering`过程，以减少因矩阵的大稀疏性而产生的缓存丢失。

有时建议在使用迭代求解器求解之前准备好系统矩阵。这个过程被称为`preconditioning`，包括将方程组转换为等价但条件更好的方程组，以便用迭代求解器求解。 [用于对角线主导系统的`Diagonal`预处理程序[^90]，用于一般非对称系统的带容限和填充的不完全$L U$，以及用于对称系统的`Incomplete Cholesky`[^90]是流行预处理程序的例子。不幸的是，为某个问题找到求解器和预处理器的最佳组合是一个经验问题，没有一个适合所有问题的最佳组合。

\textbf{Calculating Additional Results} 

在一个线性问题中，在解决了全局方程组之后，主要的结果就得到了，在某种意义上问题就解决了。但在许多情况下，还有一些额外的结果是值得关注的，必须进行计算。例如，在结构分析中，节点位移可以通过求解全局方程组得到。然而，元素中的应力也很重要，必须进行计算。这些值通常用主要结果计算，即每个元素的位移。例如，结构问题中的元素应力可以使用位移值$\mathbf{a}^{e}$来计算，通过使用以下方程[^75]解决全局系统得到。

\begin{equation}
\sigma=\mathbf{D B a}^{e}-\mathbf{D} \varepsilon_{0}+\sigma_{0}
\label{3.98}
\end{equation}

其中$\sigma$是元素应力，$\mathbf{D}$是弹性矩阵，$\mathbf{B}$是应变矩阵，$\varepsilon_{0}$是初始应变，$\sigma_{0}$是元素的初始应力。然而，由这个方程得到的结果通常在域上是不连续的。这意味着一个节点从与之相连的不同元素得到的应力结果是不同的。由于这个原因，不同的平均方法被实施来平滑不连续的结果。另一种方法是使用恢复方法，试图用更好的近似值再现连续梯度结果 [^104] 。

\textbf{Iterating} 

我们可以注意到，前面的章节主要是基于线性微分方程的。那么，如果问题不是线性的，该怎么做呢？有几种方法可以处理非线性问题。不幸的是，这些方法不能像以前那样简单地获得结果，通常需要进行迭代才能找到结果。考虑到以下非线性方程组。

\begin{equation}
\mathbf{K}(u) \mathbf{u}=\mathbf{f}
\label{3.99}
\end{equation}

人们可以计划一个迭代求解程序，其中每组的未知数$\mathbf{u}_{n}$被用来计算方程组，并计算下一组的未知数$\mathbf{u}_{n+1}$ 。

\begin{equation}
\mathbf{K}\left(u_{n}\right) \mathbf{u}_{n+1}=\mathbf{f}
\label{3.100}
\end{equation}

有几种方法可以加速这个程序的收敛。一些例子是 `Newton method` , `Modified Newton method` , `Line search` [^104] ，等等。如前所述，所有这些方法都需要对解决方案进行迭代。

\subsection{Multi-Disciplinary Problems} 

这项工作的目的是创建一个框架来处理多学科问题。因此，在进一步了解之前，有必要对这些问题做一个总体描述，并简要描述它们的一些重要特征。

\subsubsection{Definitions} 

对于多学科问题有不同的定义。一个多学科的解决方案通常被定义为一起解决不同物理模型的耦合系统。一个耦合系统被认为是定义模型的依赖性问题放在一起的集合。

在这项工作中，一个多学科问题，也被称为`coupled problem`，被定义为解决一个由不同配方和算法的组件组成的模型，并在一起互动。值得一提的是，这种差异不仅来自于问题的不同物理性质，也来自于它们的不同类型的`mathematical modeling`或离散化。

一个场是代表某个数学模型的多学科模型的一个子系统。典型的例子是流体场和流体-结构相互作用问题中的结构场。在一个耦合系统中，域是由场方程支配的建模空间的一部分，即一个结构域和一个流体域。

\subsubsection{Categories} 

为多学科问题给出的定义包括具有非常不同特征的广泛问题。这些问题可以分为不同的类别，反映出它们影响解决程序的一些方面。一种分类可以通过不同的子系统如何相互作用来进行。另一种分类可以反映领域界面的类型。

\textbf{Weak and Strong Coupling} 

人们可以通过不同子系统之间的耦合类型对多学科问题进行分类。考虑一个有两个相互作用的子系统的问题，如图所示 $3.9$ 。

该问题是计算子系统 $u_{1}$ 和 $u_{2}$ 在外加力 $F(t)$ 作用下的解决方案 $S_{1}$ 和 $S_{2}$ 。子系统之间有两种类型的依赖关系。

\textbf{Weak Coupling} 也称为`one-way` 耦合，其中一个领域依赖于另一个领域，但这可以独立解决。热-结构问题是这种类型的耦合的一个好例子。在这个问题中，结构的材料属性取决于温度，而热场可以独立解决，假设结构变形引起的温度变化非常小。图[3.10]显示了这种类型的耦合。

\textbf{Strong Coupling}也被称为`two-way`耦合，当每个系统都依赖于另一个系统，因此它们都不能被单独解决。具有大变形的结构的流体-结构相互作用问题就属于这一类。在这些问题中，结构在来自流体的压力下发生变形，流体的速度和压力取决于变形结构的形状。图[3.11]显示了这种类型的耦合。

\textbf{Interaction Over Boundary and Domain} 

如前所述，多学科问题的一个分类依赖于子系统如何相互作用。另一种分类可以不看它们如何互动，而是看它们在哪里相互作用。使用这一标准的多学科问题有两类 [^104] 。

\textbf{Class I} 在这一类别中，相互作用发生在各领域的边界。例如，在流体-结构相互作用问题中，相互作用发生在与流体接触的结构的边界，反之亦然。图[3.12]显示了这种类型问题的一个例子。

\textbf{Class II}这类问题包括领域可以完全或部分重叠的问题。热力-流体问题就是这类问题的一个很好的例子，流体的领域和热力问题重叠了。图[3.13]显示了这类问题的一个例子。

\subsubsection{Solution Methods} 

有几种不同的方法来解决多学科的问题。为每个案例找到合适的方法，高度依赖于问题的类别和每个领域的不同细节，特别是对时间相关问题。在本节中，将讨论解决这些问题的不同方法的概述。

\textbf{Sequential Solution of Problems with Weak Coupling} 

`one-way` 耦合问题的求解过程是微不足道的。考虑图[3.10]的问题，有两个子系统$S_{1}$和$S_{2}$，其中$S_{2}$取决于$u_{1}$（$S_{1}$的解）。这个问题可以通过先解决$S_{1}$并使用其解$u_{1}$来解决$S_{2}$来轻松解决，如图3.14所示。对于瞬态问题，这可以在每个时间步长进行。













\textbf{Monolithic Approach} 

在这种方法中，相互作用的场被一起建模，从而形成一个耦合的连续模型，最后形成一个可直接使用的多学科元素。考虑图[3.11]中的强耦合问题，不仅子系统$S_{2}$取决于子系统$S_{1}$的解，而且子系统$S_{1}$也取决于$S_{2}$。


\begin{align}
&\mathcal{L}_{1}\left(u_{1}, u_{2}, t\right)=f_{1}(t)  \label{3.101}\\
&\mathcal{L}_{2}\left(u_{1}, u_{2}, t\right)=f_{2}(t) \label{3.102}
\end{align}


应用时间和空间的离散化，我们可以将上述方程改写为每个时间步骤的以下形式。

\begin{equation}
\left[\begin{array}{ll}
\mathbf{K}_{1} & \mathbf{H}_{1} \\
\mathbf{H}_{2} & \mathbf{K}_{2}
\end{array}\right]\left[\begin{array}{l}
\mathbf{u}_{1} \\
\mathbf{u}_{2}
\end{array}\right]=\left[\begin{array}{l}
\mathbf{f}_{1}(t) \\
\mathbf{f}_{2}(t)
\end{array}\right]
\label{3.103}
\end{equation}

其中$\mathbf{K}_{1}$和$\mathbf{K}_{2}$是对应于场变量的场系统矩阵，$\mathbf{H}_{1}$和$\mathbf{H}_{2}$是对应于交互变量的场系统矩阵。这些方程可以在每个时间步长进行求解，以计算出两个场的解决方案。图[3.15]显示了这个方案。

尽管这种方法看起来非常容易和自然，但在实践中它遇到了困难。一个问题是表述上的困难。多学科的连续模型，本质上通常是复杂的，这种复杂性使得离散化过程成为一项繁琐的任务。然而通过使用计算机代数系统，如`Mathematica` [^103] 和`Maple` [^66]，这种方法的符号推导变得更加可行。

















另一个问题是全球系统的大小和带宽。在这种方法中，所有的领域都必须一起解决，这使得它成为一种昂贵的方法。

另一个缺点是实施成本。实现某个字段与任何新字段的交互需要定制接口矩阵$\mathbf{H}_{1}$和$\mathbf{H}_{2}$，以反映新的变量。这些不能重复用于该字段的另一个交互。此外，任何现有的用于解决每个领域的代码都不能被重新使用，在许多情况下，为了使它们适应这种方法，必须进行严格的修改。

尽管有这些问题，这种方法完美地模拟了相互作用，并产生了解决耦合问题的更强大和更稳定的公式。

\textbf{Staggered Methods} 

`staggered methods`的意图是单独解决每个场，并通过应用不同的技术将变量从一个场转换到另一个场来模拟相互作用。下面介绍一些常见的交错方法的技术。

\textbf{Prediction} 这种技术包括预测下一步中因变量的值。例如在图[3.11]的`two-way coupled problem`中，对变量$u_{2}^{(n+1)}$的预测可以用来单独解决$S_{1}$子系统。这种技术被广泛用于在强耦合的问题中解耦不同的场。图[3.16]显示了预测的方案。

有不同的方法来预测下一步的解决方案。一种常见的方法是`last-solution predictor`，它使用变量的实际值作为下一步的预测值。

\begin{equation}
u_{p}^{(n+1)}=u^{(n)}
\label{3.104}
\end{equation}

另一种常见的选择是通过解梯度预测，它将预测的变化应用于变量的实际值，以获得下一步的预测值。

\begin{equation}
u_{p}^{(n+1)}=u^{(n)}+\Delta t \dot{u}^{(n)}
\label{3.105}
\end{equation}

与

\begin{align}
\Delta t &=t^{(n+1)}-t^{(n)}  \label{3.106}\\
\dot{u}^{(n)} &=\left(\frac{\partial u}{\partial t}\right)^{(n)} \label{3.107}
\end{align}


\textbf{Advancing} 使用其他子系统的计算或预测的解决方案计算子系统的下一个时间步骤。图[3.17]显示了这种技术。

\textbf{Substitution} 替换是一种微不足道的技术，它将一个字段的计算值用于另一个字段，以分别求解它。图[3.18]显示了其方案。

\textbf{Correction} 考虑到$S_{1}$子系统，该系统使用预测解$u_{2 p}^{(n+1)}$进行求解，得到$u_{1}^{(n+1)}$ 。使用$u_{1}^{(n+1)}$推进子系统$S_{2}$，得到$u_{2}^{(n+1)}$ 。修正步骤包括用$u_{2}^{(n+1)}$代替预测值$u_{2 p}^{(n+1)}$并再次求解$S_{1}$以获得更好的结果。很明显，这个过程可以重复几次。图[3.19]显示了这个程序。

可以用上述技术规划出一种交错的方法。例如，回到图[3.11] 的问题，我们可以计划以下的交错方法。

1. 预测。 $u_{p}^{(n+1)}=u_{2}^{(n)}+\Delta t \dot{u}_{2}^{(n)}$ 

2. 推进。 $S_{1}^{(n+1)}\left(u_{p}^{(n+1)}\right) \rightarrow u_{1}^{(n+1)}$ 

3. 取代。 用$u_{1}^{(n+1)}=u_{1}^{(n+1)}$代替$S_{2}$。

4. 推进。 $S_{2}^{(n+1)}\left(u_{1}^{(n+1)}\right) \rightarrow u_{2}^{(n+1)}$ 












图[3.20]显示了这个程序。

关于交错方法及其技术的更多信息可以在 [^41][^42] 中找到。

交错方法比单一方法使用更少的资源，因为每一步只解决问题的一个部分。这在解决大型问题时是一个很大的优势。同时，这也提供了一个思路，即在几台机器上并行地使用相同的程序来解决大型单场问题。

另一个优点是可以重复使用现有的单领域代码来解决多学科的问题，几乎不需要修改。这可以通过编写一个小程序来控制每个领域的独立程序之间的互动来实现。这种策略被称为`master and slave method`，广泛用于解决多学科问题。

这种方法还可以为每个领域使用不同的离散化。它还可以让每个领域有自己的网格特征。这对于解决大型复杂的耦合问题是一个很大的附加值。

像往常一样，除了所有的优点之外，也有一些缺点。交错方法需要仔细制定以避免不稳定并获得准确的解决方案。一般来说，交错方法不如单体方法稳健，在建模和求解时需要更多关注。








\subsection{Programming Concepts} 

设计和实现一个新的软件是一项艰难的任务。使用适当的软件工程解决方案和先进的编程技术可以显著提高程序的质量。本章介绍了不同的软件工程解决方案和编程技术，对设计有限元程序很有用。

\subsubsection{Design Patterns} 

设计通常由几个决定组成，这些决定会影响到代码的功能复用性、灵活性和可扩展性。然而，有几个经典的问题在设计过程中出现，可以通过应用现有的 `Design Patterns` 轻松解决。设计模式是一些可重复使用的模式，用于设计程序的一部分，代表一个已知的问题。在这一节中，简要地解释了可用于设计一套有限元程序的模式。

\textbf{Strategy Pattern} 

 `Strategy Patterns`通过将每个算法封装在一个单独的类中，并通过基类建立的统一接口使它们可以互换，从而定义了一个算法系列。图[3.21]显示了这种模式。





在这个结构中`Strategy`声明了所有策略的接口。 `User`有一个对`Strategy`对象的引用，并使用`Strategy`接口来调用该算法。 `User`也可以让`Strategy`通过接口访问其数据。最后每个`ConcreteStrategy`使用`Strategy`接口实现一个算法。

在有限元程序设计中，有许多点可以使用这种模式。线性求解器、几何体、元素、条件、过程、策略等等。图[3.22]显示了一个使用此模式设计线性求解器结构的例子。





使用这种模式，每个派生的类`LinearSolver`都分别封装了一种解算算法。这种封装使一个库更容易扩展。该接口由Linearsolver基类定义，对所有派生求解器来说是统一的。 `User`保持一个指向`LinearSolver`基类的指针，该指针可以指向求解器家族的任何成员，并使用基类的接口来调用不同的程序。

\textbf{Bridge pattern} 

`bridge`模式将抽象和它的实现解耦，其方式是它们可以独立变化。图[3.23]显示了这个模式的结构。

在这个模式中`Abstraction`为用户定义了接口，同时也持有实现者的引用。 `AbstractionForm` 创建一个新的概念，也可以扩展`Abstraction`的接口。





 `Implementor` 定义了实现部分的接口，它被抽象所使用。每个`ConcreteImplementor`为具体案例实现了实现者接口。

桥梁模式对于连接具有层次结构的概念非常有用。在一个有限元程序中，这种模式可以用来连接元素和几何体，线性求解器和它们的重排序器，或者连接迭代求解器和预处理器。

例如，将`bridge`模式应用于`Element`的结构，会产生图[3.24]中所示的结构。

这个模式让每个`Element`将其表述与任何`Geometry` .

\textbf{Composite Pattern} 

`Composite` 模式让用户将一组的对象组合在一个复合对象中，并统一处理单个对象和对象的组合。图[3.25]显示了这个模式的结构。

在这个模式中，组件定义了对象操作的接口，也声明了访问和管理子组件的接口。它也可以定义一个接口，用于在可逆结构中访问该组件的父级。 `Leaf`没有子组件，只实现了操作。它可以作为组合中的基本单元。 `Composite` 存储其子代，并实现子代管理接口。它还通过使用其子代的操作来实现其操作。最后用户可以使用组件接口来统一处理组合中的所有对象。

这种模式可以用来设计流程，这些流程可以由流程的集合构建，几何体可以将它们组合成一个复合体，甚至元素也可以将不同的元素组合在一起并混合计算。

例如，将这种模式应用于过程的结果是图[3.26] 所示的结构。

这种结构使用户能够将不同的过程结合在一起，并像其他过程一样使用。

\textbf{Template Method Pattern} 

`Template Method` 模式单独定义了一个算法的骨架，并将一些步骤推迟到子类。通过这种方式，模板方法模式允许子类在不改变算法结构的情况下重新定义算法的某些步骤。图[3.27]显示了这种模式的结构。

在这个结构中`AbstractClass`定义了抽象的原始操作，也在模板方法中实现了算法的骨架。 `ConcreteClass`实现了原始操作，这些原始操作将被模板方法用作算法的变化步骤。

当各种算法在某些步骤上有差异，但在整体上没有差异时，这种方法就很有用。可以使用这种模式设计策略，以提供一类可由方案改变的算法，或者将这种模式应用于线性求解器的设计，可以使它们独立于矩阵和向量及其操作。

图[3.28]显示了在设计策略时使用这种模式的一个例子。

\textbf{Prototype Pattern} 

`Prototype`模式提供了一组待创建对象的原型。 `User` 克隆原型以创建该类型的新对象。系统可以扩展到任何有原型的新类型。图[3.29]显示了这种模式的结构。

 `Prototype` 提供了克隆接口，是创建原型的通用基类 `list` 。每个具体的原型都为自己实现了克隆操作。 `User` 通过要求其相关的原型克隆自己来创建一个新对象。

 `Prototype` 模式对于设计一个可扩展的 IO 非常有用。 IO 可以使用新对象的原型并顺利地创建它。图[3.30]显示了这种模式在IO中的使用，用于创建元素。





\textbf{Interpreter Pattern} 

`Interpreter`模式为给定的语法创建一个表示法，然后用它来解释语言。这种模式有简单的结构，如图所示 $3.31$ 。

 `AbstractExpression` 定义了语法树中所有节点的`Interpret`接口。 `TerminalExpression` 表示语法中的终端符号，并为其实现`Interpret` 方法。对于一个句子中的每个终端符号，都必须创建一个实例。 `NonterminalExpression` 表示上下文自由语法中的非终端符号。它持有其句子中所有表达式的实例。它还实现了`Interpret`方法，通常包括调用其成员`Interpret`方法。
















\textbf{Curiously Recursive Template Pattern} 

`Curiously Recursive Template` (CRT)模式，也被称为Barton和Nackman Trick，包括将派生类作为其模板参数给其基类。这个想法是根据派生类来配置基类，并提供一种静态多态性，通过派生和覆盖虚拟函数，这比通常的多态性要高效得多。图[3.32]显示了这种模式。

使用这种模式可以让开发者在不损失操作效率的情况下定制基类。当操作非常简单而虚拟函数调用的开销相当大时，这种模式更加有效。例如，一个矩阵库可以使用这种模式来让`symmetric matrix`派生类改变基类的操作符。通过这种方式，像访问方法、赋值等方法可以被重写，而不会产生性能开销。图[3.33]显示了应用于上述矩阵例子的这种模式。

本节描述的模式是在设计 Kratos 时使用的。其他模式的描述和前面提到的模式的更详细解释可以在 [^45] 中找到。

\subsubsection{C+ $+$ advanced techniques} 

性能和内存效率是有限元程序的两个关键要求。事实证明，在$\mathrm{C}++$中对数值方法的优化实现可以提供与Fortran实现相同的性能[^100]，通常$\mathrm{C}++$代码的低效率来自于开发者对语言的误解[^54] 。由于这个原因，看看用于实现高性能和高效数字算法的不同技术$\mathrm{C}++$会有帮助。这些技术被用于 Kratos 的不同部分，以提高其效率，同时提供一个清晰和易于使用的界面。







\textbf{Expression Templates} 

 `Expression Templates` 是一种技术，用于以一种非常有效的方式将表达式传递给函数参数 [^97] 。例如，将一个函数传递给一个要集成的程序。这种技术也被用于高性能的线性代数库中，以评估矢量表达式 [^98] 。通过这种方式，由对矩阵和向量的操作组成的表达式可以在不创建任何临时对象的情况下在一个循环中被评估。

我们的想法是为每个运算符创建一个模板对象，并通过结合这些模板和它们的相对变量来构造整个表达式。例如，考虑函数$f$如下:

\begin{equation}
f(x, y)=\frac{1}{x+y}
\end{equation}

将这个函数转换为其表达式模板形式可以通过三个步骤完成。 首先， 我们需要表达式来表示常数和变量，如下所示。


~~~C++
class ConstantExpression
{
    double mValue;

public:
    ConstantExpression(double Constant) : mValue(Constant) {}
    double Value() { return mValue; }
};
class ReferenceExpression
{
    double &mReference;

public:
    ReferenceExpression(double &Variable) : mReference(Variable) {}
    double Value() { return mReference; }
};
~~~



然后需要一些模板来处理运算符。

~~~C++
template <class TExpression1, class TExpression2>
class
    SumExpression
{
    TExpression1 mExpression1;
    TExpression2 mExpression2;

public:
    SumExpression(TExpression1 Expression1,
                  TExpression2 Expression2) : mExpression1(Expression1),
                                              mExpression2(Expression2) {}
    double Value() { return mExpression1.Value() +
                            mExpression2.Value(); }
};
template <class TExpression1, class TExpression2>
class
    DivideExpression
{
    TExpression1 mExpression1;
    TExpression2 mExpression2;

public:
    DivideExpression(TExpression1 Expression1,
                     TExpression2 Expression2) : mExpression1(Expression1),
                                                 mExpression2(Expression2) {}
    double Value() { return mExpression1.Value() /
                            mExpression2.Value(); }
};
~~~




而最后表达式模板版本可以使用之前的组件来编写。转换x+y表达式的结果是:

~~~C++
SumExpression<ReferenceExpression, ReferenceExpression>(ReferenceExpression(x), ReferenceExpression(y));
~~~


 

使用上述表达式，整个函数可以写成如下:


~~~C++
typedef SumExpression<ReferenceExpression,
                      ReferenceExpression>
    sum_expression;
typedef DivideExpression<ConstantExpression,
                         sum_expression>
    expression;
expression f = expression(ConstantExpression(1),
                          sum_expression(ReferenceExpression(x),
                                         ReferenceExpression(y)));
~~~


手动编写这些表达式确实不切实际，但幸运的是，仔细的重载运算符可以自动完成这种转换。如前所述，这种技术可以用来评估矩阵和向量表达式，而不需要创建临时性的表达式，而且是一次完成。在向量或矩阵上使用重载运算符的简单求和操作会导致许多多余的循环和开销。例如，考虑到下面这段无辜的代码。

~~~C++
// a,b,c and d are vectors
d = a + b + c;
~~~

这段简单的代码对三个向量求和，并使用简单的重载运算符将其赋值给另一个向量，可以产生相当于以下的代码。

~~~C++
Vector t1 = b + c; Vector t2 = a + t1; d = t2;
~~~

在第一步中，重载运算符被用来计算两个向量的和，并把它们放在临时的`vector` t1中。再次使用重载运算符计算`vector`a和临时`vector`t1的总和，结果存储在另一个临时`vector`$t 2$中。最后，第二个暂存器 `vector` 被分配到左手边 `vector` $\mathrm{d}$ 。可以看出，这个操作是通过在所有`vector`元素上进行三次循环和创建两个临时向量来完成的，这使得效率非常低。使用表达式模板可以消除所有这些开销，使其与手工编码的程序一样高效。

这个想法是通过重载操作符来创建表达式，而不对其进行评估。表达式的评估被推迟到赋值时进行。这个表达式的右侧可以转换为以下形式。


~~~C++
class ReferenceExpression
{
    Vector &mReference;

public:
    ReferenceExpression(Vector &Variable) : mReference(Variable) {}
    double Value(int i) { return mReference[i]; }
};
template <class TExpression1, class TExpression2>
class
    SumExpression
{
    TExpression1 mExpression1;
    TExpression2 mExpression2;

public:
    SumExpression(TExpression1 Expression1,
                  TExpression2 Expression2) : mExpression1(Expression1),
                                              mExpression2(Expression2) {}
    double Value(int i) { return mExpression1.Value(i) +
                                 mExpression2.Value(i); }
};
typedef SumExpression < ReferenceExpression,
    SumExpression<ReferenceExpression,
                  ReferenceExpression>
        rhs_expression;
d = rhs_expression(ReferenceExpression(a),
                   SumExpression<ReferenceExpression,
                                 ReferenceExpression>(b, c));
~~~



现在一个重载的赋值运算符可以完成这个程序。

~~~C++
template <class TExpression>
operator=(Vector &a, TExpression Expression)
{
    for (int i = 0; i < size; i++)
        a[i] = Expression.Value(i);
}
~~~


将我们的`rhs_expression`传递给这个赋值运算符，结果是一个相当于的代码。

~~~C++
for (int i = 0; i < size; i++)
    d[i] = rhs_expression.Value(i);
~~~

内联第一个Value方法和它里面的引用，结果是下面的代码。
~~~C++
for (int i = 0; i < size; i++)
    d[i] = a[i] + SumExpression<ReferenceExpression,
                                ReferenceExpression>(b, c)
                      .Value(i);
~~~


而内联第二个Value方法和它的所有引用的结果是优化的代码。
~~~C++
for (int i = 0; i < size; i++)
    d[i] = a[i] + b[i] + c[i];
~~~


\textbf{Template Metaprogramming} 

模板被添加到$\mathrm{C}++$中，以更好地替代旧C中现有的宏。最终，他们没有消除宏的使用，但给我们带来了比任何人都更多的期望。例如，模板元编程。一切都始于Erwin Unruh欺骗编译器在编译时打印一个`list`的素数。这将算法的编写从$\mathrm{C}++$中的标准形式扩展到一个新的形式，使编译器运行该算法，并导致一个新的特定算法运行。

模板元编程技术可用于在编译时创建一个专门的算法。这种技术使编译器解释$\mathrm{C}++$代码的一个子集，以便生成这种专门的算法。不同的方法被用来在编译时模拟不同的编程语句，如循环或条件语句。这些语句被用来告诉编译器如何为我们的特定案例生成代码。

这种技术使用递归模板来鼓励编译器进行循环。当一个模板递归地调用自己时，编译器必须做一个实例化模板的循环，直到达到一个作为停止规则的专门版本。下面是一个模板元编程的例子，可以用来做一个编译时的幂函数。 首先需要一个通用的模板函数类来容纳幂函数类。


~~~C++
template <std::size_t TOrder>
struct Pow
{
    static inline double Value(double X)
    {
        // Calculatin result ...
        return result;
    }
};
~~~



这个类将顺序作为类的模板参数。注意，`TOrder`必须是一个正整数，而且在编译时也是已知的。所以它不能作为一个正常的运行时幂函数使用。现在我们采取一个递归算法来计算一个值$n$的第-次幂$x$ 。


\begin{align}
&x_{i}=x_{i-1} * x, i=2, \ldots, N \label{3.108a}\\
&x_{1}=x \label{3.108b}
\end{align}



使用模板元编程来实现这一点是相对简单的。递归模板可以在这里使用，使编译器执行一个for循环，并在每一次传递中向我们的代码添加一个$x$的乘法。在重复$k$次后停止循环，将产生一个相当于手动编写的代码。 首先， 我们在Value方法中引入了递归部分。

~~~C++
template <std::size_t TOrder>
struct Pow
{
    static inline double Value(double X)
    {
        return X * Pow<TOrder - 1>::Value(X);
    }
};

~~~



而方程 \ref{3.108b} 中的停止规则是一个专门的模板

~~~C++
template <>
struct Pow<1>
{
    static inline double Value(double X)
    {
        return X;
    }
};
~~~


现在幂函数已经可以使用了。下面是一个调用它来计算的例子 $x^{4}$ 。

~~~C++
double x = 2.00; double y = Pow <4>:: Value(x); assert(y == 16.00)
~~~


在编译的时候，编译器会尝试内联Value函数，结果是

~~~C++
double y = x * Pow<3>::Value(x)
~~~

然后继续递归调用order 1

~~~C++
double y = x * x * x * Pow<1>::Value(x)
~~~


在这一点上，模板专业化阻止它进入无限循环，因为 `Pow<1>` 的Value方法不是递归的。编译器试图内联这个Value方法，并生成了以下代码。

~~~C++
double y = x * x * x * x;
~~~


使用这个类有一个好处。如果$\mathrm{x}$在编译时已经知道，$\mathrm{y}$也可以在编译时计算。这可以用来优化代码，例如，当重复数是一个已知值时，循环可以被解开，等等。

模板专业化也可以用来让编译器模拟条件语句或switch和case。这些语句可以用来根据一些条件生成不同的代码。例如，一个矩阵的赋值运算符可以根据给定矩阵的行或列的多数来改变其算法。

~~~C++
template <bool c>
class Assign
{
};
class Assign<true>
{
public:
    template <class Matrix1, class Matrix2>
    Assign(Matrix1 &A, Matrix2 &B)
    {
        // Assigning row by row;
    }
} class Assign<false>
{
public:
    template <class Matrix1, class Matrix2>
    Assign(Matrix1 &A, Matrix2 &B)
    {
        // Assigning column by column;
    }
} template <class Matrix2>
operator=(Matrix2 &B)
{
    Assign<Matrix2 ::IsRowMajor>(*this, B);
}

~~~



在这种形式下，编译器为每个赋值语句生成一个专门的算法。





\section{General Structure} 

本章首先描述了设计Kratos的目标和考虑的用户，然后给出了设计结构的方法。

\subsection{Kratos' Requirements} 

 Kratos被设计为建立多学科有限元程序的框架 [^31] 。设计和实现中的通用性是首要要求。 Kratos 必须提供有限元求解所需的通用工具。它还必须消除其他代码中存在的许多限制，以实现足够的灵活性，处理比以前更多的算法。例如，像使用某些自由度（Dof）的限制。

 Kratos必须提供一个灵活的结构，以便处理各种各样的方法和算法。这种灵活性不仅要在其全局布局和基本假设中提供，也要在其实施细节中提供。尽量减少限制性假设是必要的，以便让开发者在不同层次上按自己的意愿配置这个库。

 Kratos作为一个库必须在其提供的工具中提供良好的可重用性。这里的关键点是帮助用户使用Kratos提供的通用组件，甚至是其他应用程序，更容易和更快地开发自己的有限元代码。

 Kratos必须是可扩展的，在不同的实现层面。它必须可以扩展到新的公式、算法和概念。支持在多学科问题中可以耦合的各种问题，需要非常不同的公式和算法来实现。这些公式和算法也可能使用新的概念和变量。所以Kratos必须为其所有组件提供可扩展的设计，以支持新的方法。

另一个重要的要求是良好的性能和内存效率。这种特性对于使使用Kratos实现的应用程序能够处理工业多学科问题是必要的。这些要求非常重要，是 Kratos 中大多数限制的原因。

最后，它必须提供不同层次的开发者对Kratos系统的贡献，并在他们扩展系统的方式上匹配他们的要求和困难。开发者可能只想做一个插件扩展，在其上创建一个应用程序，或者使用IO脚本来使Kratos执行某种算法。 Kratos不仅要提供所有这些功能，而且要把不必要的困难隐藏起来，不让每组开发者知道。

\subsection{Users} 

设计中的一个重要因素是确定谁将与该程序一起工作，他们的需求是什么，以及该程序如何能帮助他们。实质上Kratos被定义为被三组不同层次的用户所使用。

\textbf{Finite `Element` Developers} Kratos被定义为由有限元开发人员使用，以轻松实现多学科的配方。这些开发人员，或从Kratos的角度来看的用户，从物理和数学的角度来看，被认为是比$\mathrm{C}++$编程更专业的有限元。由于这个原因，Kratos必须提供他们的要求，而不涉及高级编程概念。

\textbf{Application Developers} Kratos可以作为其他应用程序的有限元引擎。这种能力有利于另一个团队的开发人员与 Kratos 一起工作。这些用户对有限元编程不太感兴趣，他们的编程知识可能从非常专业到高于基础的程度不等。他们不仅可以使用Kratos本身，还可以使用有限元开发者提供的任何其他应用程序，或其他应用程序开发者。优化程序或设计工具的开发者是这类典型的用户。

\textbf{Package Users} 工程师和设计师是Kratos 的其他用户。他们使用Kratos的完整包和它的应用程序来建模和解决问题，而不参与这个包的内部编程。对于这些用户，Kratos必须提供一个灵活的外部接口，使他们能够使用Kratos的不同功能而不改变其实现。

 Kratos必须提供一个框架，以便前面提到的具有完全不同专业领域的开发人员团队在上面工作，以创建多学科的有限元应用。

\subsection{Object Oriented Design} 

面向对象的有限元程序设计的历史可以追溯到90年代初，甚至更早。在此之前，许多大型有限元程序是以模块化方式开发的。工业界一方面要求解决更复杂的问题，另一方面又要求维护和扩展以前的程序，这使得开发人员将他们的设计策略定位于面向对象 [^44][^43][^64][^80][^82] 。

面向对象结构的主要目标是将整个问题分割成几个对象并定义它们的接口。对于我们想要编程的每一种问题，都有许多可能的方法，结果结构的功能主要取决于它。就有限元问题而言，也有许多方法，如基于偏微分方程求解方法[^22]或在有限元方法本身构建对象[^44][^106][^35][^34] 。

在Kratos中，我们选择了第二种方法，并基于有限元的一般方法来构建我们的对象。选择这种方法是因为我们的目标是为多学科问题创建一个有限元环境。另外，我们的同事一般来说对这种方法比对物理特性更熟悉。此外，这种方法给了我们上述目标中提到的必要的通用性 Kratos 。在这个范围内，主要对象来自有限元结构的各个部分。然后，为了实现的目的，定义了一些抽象的对象。最后，他们的关系被定义，他们的责任被平衡。图[4.1] 显示了 Kratos 中的主要类。




 `Vector`、`Matrix`和`Quadrature`是由基本数字概念设计的。 `Node` , `Element` , `Condition` , 和 `Dof` 是直接由有限元概念定义的。 `Model`, `Mesh`, 和`Properties`来自有限元建模中使用的实际方法，由`ModelPart`, 和`SpatialContainer`完成，用于更好地组织分析所需的所有数据。I0, `LinearSolver` , `Process` , 和 `Strategy` 代表了有限元程序流程的不同步骤。最后 `Kernel` 和 Application 被定义为库管理和定义其接口。

这些主要对象描述如下:

\textbf{Vector} 代表代数`vector`并定义了向量上的常用运算符。

\textbf{Matrix} 封装了矩阵及其运算符。有不同的矩阵类别是必要的。最典型的是密集矩阵和压缩行矩阵。

\textbf{Quadrature} 实现有限元方法中使用的正交方法。例如，不同积分点数量的高斯积分。

\textbf{Geometry} 在一个`list`点或`Nodes`上定义一个几何体，并从其通常的参数如面积或中心点提供给`shape functions`和坐标转换程序。

\textbf{Node} `Node`是一个具有额外设施的点。存储节点数据、历史节点数据和自由度的`list`。它还提供了一个接口来访问它的所有数据。

\textbf{Element}在一个对象中封装了元素公式，并提供了一个接口来计算组装全局方程组所需的局部矩阵和向量。它持有其几何图形，同时是其数组 `Nodes` 。还存储了元素数据和访问它的接口。

\textbf{Condition} 封装了计算全局方程组中`Condition`的局部贡献所需的数据和操作。 `Neumann ` 条件是`Conditions`的例子，可以通过该类的导数进行封装。

\textbf{Dof} 代表一个自由度（自由度）。它是一个轻量级的对象，持有其变量，如`TEMPERATURE`，其自由状态，以及对其数据结构中数值的引用。这个类使系统能够与不同的的自由度集合一起工作，也表示分配给每个自由度的`Dirichlet`条件。

\textbf{Properties} 封装了不同的`Elements`或`Conditions`共享的数据。它可以存储任何类型的数据，并提供一个变量基础访问它们。

\textbf{Model} 存储要分析的整个模型。所有 `Nodes` , `Properties` , `Elements` , `Conditions` 和解决方案数据。它还提供了这些数据的访问接口。

\textbf{ModelPart} 保存与模型的任意部分相关的所有数据。它存储所有现有的组件和数据，如`Nodes`、`Properties`、`Elements`、`Conditions`和与模型某部分相关的解决方案数据，并提供以不同方式访问它们的接口。

\textbf{`Mesh`} 保存`Nodes`、`Properties`、`Elements`、`Conditions`，代表模型的一部分，但没有额外的解决方案参数。它提供对其数据的访问接口。

\textbf{SpatialContainer} 与空间搜索算法有关的容器。这种算法对于寻找离某个点最近的`Node`或`Element`或其他空间搜索很有用。Quadtree和Octree是这些容器的例子。

\textbf{IO} 提供不同的输入输出程序的实现，可用于不同格式和特性的读写。

\textbf{LinearSolver} 封装了用于解决线性方程组的算法。不同的直接求解器和迭代求解器可以在Kratos中实现，作为这个类的衍生物。

\textbf{Strategy} 封装了解算算法和解算过程的一般流程。 `Strategy` 管理方程组的建立，然后使用线性求解器进行求解，最后负责更新数据结构中的结果。

\textbf{Process} 是为Kratos添加新算法的扩展点。 `Mapping` 算法、优化程序和许多其他类型的算法可以作为 Kratos 中的一个新过程来实现。

\textbf{Kernel} 通过初始化不同的部分来管理整个 Kratos ，并提供必要的接口与应用程序进行通信。

\textbf{Application} 提供所有必要的信息，以便将一个应用程序添加到 Kratos 。从它派生出来的类有必要给内核它所需要的信息，如new `Variables` , `Elements` , `Conditions` 等。

这里的主要意图是向开发者隐藏所有困难但常见的有限元实现，如数据结构和 IO 编程。

\subsection{Multi-Layers Design} 

 Kratos在其设计中采用了多层方法。在这种方法中，每个对象只与本层或本层以下的其他对象接口。还有一些其他的分层方法限制了两层对象之间的接口，但在Kratos中没有应用这种限制。分层减少了程序内部的依赖性。它有助于代码的维护，也有助于开发人员理解代码并明确他们的任务。在设计结构层时，考虑了前面提到的不同用户。分层是以一种方式进行的，即每个用户必须在尽可能少的层数中工作。通过这种方式，每个用户需要知道的代码量被最小化，不同类别的用户之间发生冲突的机会也被减少。这种分层也让Kratos根据工作在其中的用户的知识来调整每一层所需的实施难度。例如，有限元层只使用$\mathrm{C}++$编程的基本到一般的功能，但主要开发者层使用高级语言功能，以提供理想的性能。

按照前面提到的当前设计，Kratos被组织成以下几层。

\textbf{Basic Tools Layer} 保存了Kratos 中使用的所有基本工具。在这一层中，为了使这些工具的性能最大化，使用C++的先进技术是必不可少的。这一层被设计成由专家级的程序员来实现，并且只需要较少的FEM知识。这一层也可以提供与其他库的接口，以便从该领域的现有工作中获益。

\textbf{Base Finite Element Layer} 该层持有实现有限元公式所需的对象。它也定义了为新公式而扩展的结构。该层向有限元开发者隐藏了节点和数据结构的困难实现以及其他常见的特征。

\textbf{Finite Element Layer} 有限元开发者的扩展层。有限元层是`restricted`使用语言的基本和平均特征，并使用组件基础有限元层和基本工具来优化性能，而不进入优化细节。

\textbf{Data Structure Layer} 包含组织数据结构的所有对象。该层在实现上没有限制。先进的语言特性被用来最大化数据结构的灵活性。

\textbf{Base Algorithms Layer} 提供构建算法的可扩展结构的组件。通用算法也可以在这里实现，以帮助开发者通过重复使用它们来实现。

\textbf{User's Algorithms Layer} 另一个供有限元程序员使用的层，但在更高层次上。这一层包含了实现 Kratos 中不同算法的所有类。在这一层的实现需要中等水平的编程经验，但比有限元层的程序结构知识要高。

\textbf{Applications' Interface Layer} 这一层包含所有管理Kratos的对象以及它与其他应用程序的关系。该层的组件使用高级编程技术实现，以提供所需的灵活性。

\textbf{Applications Layer} 一个简单的层，包含了某些应用程序与Kratos 的接口。

\textbf{Scripts Layer} 保存着一些的IO脚本，可用于实现外部的不同算法 Kratos 。软件包用户可以使用这一层的模块或创建自己的扩展，而无需了解 $\mathrm{C}++$ 编程或 Kratos 的内部结构。通过这一层，他们可以激活和停用某些功能或实现一个新的全局算法，而不需要进入Kratos的实施细节。

图[4.2] 显示了 Kratos 的多层性质。





\subsection{Kernel and Applications} 

在Kratos的第一次实现中，所有的应用程序都是在Kratos中实现的，也是一起编译的。这种方法在当时产生了一些应用程序之间的冲突，并且需要许多不必要的代码重新编译以适应其他应用程序的变化。所有这些问题导致了策略的改变，不仅将每个应用程序与其他应用程序分开，而且也与Kratos本身分开。

在Kratos目前的结构中，每个应用程序都是单独创建和编译的，只是使用一个标准接口与Kratos的内核通信。通过这种方式减少了冲突，也使编译时间降到最低。应用程序类提供了将应用程序引入到 Kratos 内核的接口。 `Kernel`通过这个接口使用应用程序提供的信息来管理它的组件，配置Kratos的不同部分，并使应用程序与其他应用程序同步。应用程序类非常简单，包括注册新的组件，如。 `Variables`, `Elements`, `Conditions`等在应用程序中定义的组件。下面的代码显示了一个典型的应用类定义。


~~~C++
// Variables definition
KRATOS_DEFINE_VARIABLE(int, NEW_INTEGER_VARIABLE)
KRATOS_DEFINE_3D_VARIABLE_WITH_COMPONENTS(NEW_3D_VARIABLE);
KRATOS_DEFINE_VARIABLE(Matrix, NEW_MATRIX_VARIABLE)
class KratosNewApplication : public KratosApplication
{
public:
    virtual void Register();

private:
    static const NewElementType msNewElement;
    static const NewConditionType msNewCondition;
};
~~~


这里应用程序定义了它的新组件，现在是实现注册方法的时候了。

~~~C++
// Creating variables
KRATOS_CREATE_VARIABLE(NEW_INTEGER_VARIABLE)
KRATOS_CREATE_3D_VARIABLE_WITH_COMPONENT(NEW_3D_VARIABLE);
KRATOS_CREATE_VARIABLE(NEW_MATRIX_VARIABLE)
void KratosR1StructuralApplication ::Register()
{
    // calling base class register to register Kratos components
    KratosApplication ::Register();
    // registering variables in Kratos.
    KRATOS_REGISTER_VARIABLE(NEW_INTEGER_VARIABLE)
    KRATOS_REGISTER_3D_VARIABLE_WITH_COMPONENTS(NEW_3D_VARIABLE);
    KRATOS_REGISTER_VARIABLE(NEW_MATRIX_VARIABLE)
    KRATOS_REGISTER_ELEMENT("MyElement", msNewElement);
    KRATOS_REGISTER_CONDITION("MyCondition", msNewCondition);
}
~~~



这个接口使Kratos能够添加所有这些`Variables`、`Elements`和`Conditions`的组件。 Kratos还可以在不同的应用程序之间同步变量的编号。在Kratos中添加新的组件，使IO能够读取和写入它们，同时也配置了数据结构来容纳这些新的变量。

在下一章中，将声明基本工具层。



\section{Basic tools} 

一个有限元程序有几个常见的程序，可以作为基本工具来实现，供程序的其他部分使用。积分，计算`shape functions`和其他几何参数，以及线性求解器是这些程序的一些例子。基本工具被定义为实现这些常见的任务。实现基本工具通过消除程序中的重复程序来减少执行任务，并且通过其统一的接口增加代码不同部分之间的可重用性和兼容性。

虽然这些工具很多都是用在代码的最内部，但它们的性能却非常重要。例如，集成工具在构建时在`Elements`中被调用，其性能的任何开销都会导致程序执行时间的巨大开销。从另一个角度来看，很大一部分执行时间是由这些工具消耗的。一个很好的例子是线性求解器，它本身就占用了大量的执行时间。所有这些评论表明了在这些工具中进行性能调整的重要性。

这些工具的另一个重要要求是内存的效率。同样，在通常的有限元程序中，很大一部分内存被这些工具所消耗。这来自于两个事实，首先，大量的这些工具可能需要处理一个有限元问题，其次，它们的操作需要使用大量的内存。例如，为了对一个实际问题进行建模，必须创建许多几何图形。因此，为了控制程序使用的总内存，它们使用内存的效率是至关重要的。

最后但并非最不重要的特征是这些工具的可重复使用性。虽然这些工具必须被代码的不同部分所使用，但它们的通用性和灵活性对它们的成功使用起着重要作用。

在本节中，将解释Kratos中不同工具的设计和实现。

\subsection{Integration tools} 

对域或边界上的一些函数进行积分是有限元的基本操作之一。 `Elements` , `Conditions` 和有时代码的其他部分必须以有效的方式进行积分。所有这些都使得设计和实现一个有效的集成工具成为必要，它能以尽可能少的开销处理不同的集成方法。在开始设计集成工具之前，让我们简单了解一下有限元程序中使用的数值集成方法。

\subsubsection{Numerical Integration Methods} 

很明显，在许多情况下，对一个函数进行分析积分包含着困难，而且一般来说，并不总是可能的。这使得数值积分，也叫正交，开始于18和19世纪。然而，在实际问题上使用数值积分被推迟到计算机发展的时候。

对一个函数的积分进行近似的典型方法是在不同的点上进行评估，对它们应用特定的权重，然后将这些权重值相加，得到结果。

\begin{equation}
\int_{a}^{b} f(x) d(x) \approx \sum_{i=1}^{n} w_{i} f\left(x_{i}\right)
\label{5.1}
\end{equation}

许多经典的方法假设样本点一直处于相同的距离$h$。

\begin{equation}
x_{i}=x_{0}+i h
\label{5.2}
\end{equation}

他们使用不同数量的样本点并应用不同的加权系数来获得结果。这里有一些这类方法的例子。

\textbf{Classical Formulas} 

一个非常简单的例子是梯形规则。它采取取样点并评估它们在函数上的对应点，然后用一条线连接它们，并简单地计算在这条线下面得到的梯形的面积。换句话说，它使用这两个值的平均值来计算积分。

\begin{equation}
\int_{a}^{b} f(x) d(x)=\frac{h}{2} f(a)+\frac{h}{2} f(b)+O\left(h^{3} f^{\prime \prime}\right)
\end{equation}

不难看出，这个积分对于线性函数和其他函数都是精确的，而二阶导数是未知的，我们只需对结果进行近似计算即可

\begin{equation}
\int_{a}^{b} f(x) d(x) \approx \frac{h}{2} f(a)+\frac{h}{2} f(b)
\label{5.3}
\end{equation}

根据方程 \ref{5.1} ，我们可以为这种方法定义样本点和加权。


\begin{align}
&x_{1}=a, x_{2}=b  \label{5.5a}\\
&w_{1}=w_{2}=\frac{h}{2}  \label{5.5b}
\end{align}


著名的Simpson’s rules也属于这个类别。他们使用更多的样本点来产生高阶近似。这里是第一个。

\begin{equation}
\begin{aligned}
\int_{a}^{b} f(x) d(x) &=\frac{h}{3} f(a)+\frac{4 h}{3} f\left(\frac{a+b}{2}\right) \\
&+\frac{h}{3} f(b)+O\left(h^{5} f^{(4)}\right)
\end{aligned}
\label{5.6}
\end{equation}

它对2度以内的多项式是精确的。而它对其他函数的近似比梯形规则更好。

\begin{equation}
\int_{a}^{b} f(x) d(x) \approx \frac{h}{3} f(a)+\frac{4 h}{3} f\left(\frac{a+b}{2}\right)+\frac{h}{3} f(b)
\label{5.7}
\end{equation}

我们也可以用全局的形式来介绍。


\begin{align}
&x_{1}=a, x_{2}=\frac{a+b}{2}, x_{3}=b  \label{5.8a}\\
&w_{1}=w_{3}=\frac{h}{3}, w_{2}=\frac{4 h}{3}  \label{5.8b}
\end{align}


也有一些扩展的方法，使用同样的竞赛，使用更多的样本点，以更高的阶数进行近似。在任何情况下，要给出阶数为$n$的多项式的精确积分，这种方法需要使用$2 n+1$个样本点。这使得它们非常昂贵，而有其他方法只用$n$个样本点就能达到同样的结果，这些方法将被描述如下:

\textbf{`Gaussian Quadrature`} 

如前所述，在以前的方法中，假设样本点位于相同的距离内。如果我们利用这一限制，以其他方式选择它们的位置，我们就可以重复近似的顺序。这就是高斯正交公式的基本思想。

但这种神奇的方法是如何工作的呢？下面是对它的快速回顾。 首先 ，让我们定义两个函数$f$在$g$上的加权标量乘积为

\begin{equation}
\langle f \mid g\rangle=\int_{a}^{b} W(x) f(x) g(x) d x
\label{5.9}
\end{equation}

可以资助一组多项式，其中正好包括一个阶为$j$的多项式$p_{j}(x)$，对于每个$j=1,2,3, \ldots$，它们在给定的权重函数$W(x)$上也是相互正交的。

让我们把方程 \ref{5.1} 扩展到一个更普遍的情况，即。

\begin{equation}
\int_{a}^{b} W(x) f(x) d x \approx \sum_{i=1}^{n} w_{i} f\left(x_{i}\right)
\label{5.10}
\end{equation}

这里$W(x)$被添加为已知的加权函数，应用于我们的积分器。这个扩展是为了利用高斯正交的一个强大功能，我们可以对一个多项式乘以一些已知的加权函数进行精确的积分 $W(x)$ 。这个加权函数可以帮助我们将一些可积分的函数分解成多项式和一个复杂但可积分的部分，从而做出一个精确的积分。同时，它也可以被用来消除可整数的奇点。顺便说一下，我们可以假设方程 \ref{5.1} 是 \ref{5.10} 与$W(x) \equiv 1$的特例。选择这个加权函数的结果就是所谓的`Gauss-legendre integration` 。

现在是时候应用高斯正交的基本定理了，在区间$W(x)$内，带有加权函数$W(x)$的N点高斯正交公式的消减值正是相同区间和加权函数的正交多项式$p_{n}(x)$的根。为了给出寻找高斯权重和消减的实用方法，我们使用正交多项式集合的定义为

\begin{align}
p_{0}(x) & \equiv 1  \label{5.11a}\\
p_{1}(x) &=\left[x-\frac{\left\langle x p_{0} \mid p_{0}\right\rangle}{\left\langle p_{0} \mid p_{0}\right\rangle}\right] p_{0}(x)   \label{5.11b}\\
p_{i+1}(x) &=\left[x-\frac{\left\langle x p_{i} \mid p_{i}\right\rangle}{\left\langle p_{i} \mid p_{i}\right\rangle}\right] p_{i}(x) -\frac{\left\langle p_{i} \mid p_{i}\right\rangle}{\left\langle p_{i-1} \mid p_{i-1}\right\rangle} p_{i-1}(x) \label{5.11c}
\end{align}

可以证明，每个多项式$p_{j}(x)$正好有$j$个不同的根，而且在它们每个相邻的`pair`之间正好有一个$p_{j-1}(x)$的根。这个属性在寻找根的时候非常有用。一个寻根方案可以从$p_{1}(x)$开始应用，并继续向更高阶发展，使用之前的根的间隔来找到所有的根。最后方程 $5.10$ 被用来构成一个寻找权重的方程组 $w_{i}$ 。考虑到方程 \ref{5.10} 必须给出第一个$N-1$多项式的积分的正确答案


\begin{aligned}
a_{i j} w_{j} &=b_{i}  \label{5.12a}\\
a_{i j} &=p_{i-1}\left(x_{j}\right)   \label{5.12b}\\
b_{i} &=\int_{a}^{b} W(x) p_{i-1}(x) d x   \label{5.12c}\\
i &=1, \ldots, N, j=1, \ldots, N   \label{5.12d}
\end{aligned}


可以证明，使用相同的权重$w_{i}$，正交对所有阶数在$2 N-1$以下的多项式都是精确的。另外，在方程 \ref{5.12} 中，考虑到$i=2, \ldots, N$的正交性质，很容易验证$b_{i}$对$i=2, \ldots, N$的正交等于零。

还有另一种更实用的方法来寻找权重$w_{i}$ ，使用另一个多项式序列$\varphi_{i}(x)$ 。

\begin{align}
\varphi_{0}(x) & \equiv 0   \label{5.13a}\\
\varphi_{1}(x) &=p_{1}^{\prime} \int_{a}^{b} W(x) d x  \label{5.13b}\\
\varphi_{i+1}(x) &=\left[x-\frac{\left\langle x p_{i} \mid p_{i}\right\rangle}{\left\langle p_{i} \mid p_{i}\right\rangle}\right] \varphi_{i}(x) -\frac{\left\langle p_{i} \mid p_{i}\right\rangle}{\left\langle p_{i-1} \mid p_{i-1}\right\rangle} \varphi_{i-1}(x)   \label{5.13c}
\end{align}

其中 $p_{1}^{\prime}$ 是 $p_{1}(x)$ 相对于 $x$ 的导数。使用这些多项式计算权重是很直接的

\begin{equation} 
w_{i}=\frac{\varphi_{N}\left(x_{i}\right)}{p_{N}^{\prime}\left(x_{i}\right)}, i=1, \ldots, N   \label{5.14}
\end{equation}

还有一个公式是针对Gauss-Legendre情况的

\begin{equation}
w_{i}=\frac{2}{\left(1-x_{i}^{2}\right)\left[p_{N}^{\prime}\left(x_{i}\right)\right]^{2}}  \label{5.15}
\end{equation}

\textbf{Multidimensional Integrals} 

从一维积分到多维积分包括样本点数量的增加和应用边界的复杂性。在这一部分中，我们将只讨论一个简单的边界条件的情况。在有限元分析中，我们通常将我们的几何函数转化为一些局部坐标，然后利用该局部坐标中的简单边界将积分还原为低维度，同时考虑我们函数的平滑性。考虑到一个三维的情况

\begin{equation}
F \equiv \int_{x_{1}}^{x_{2}} \int_{y_{1}(x)}^{y_{2}(x)} \int_{z_{1}(x, y)}^{z_{2}(x, y)} f(x, y, z) d x d y d z  \label{5.16}
\end{equation}

它可以被还原成二维的形式


\begin{align}
F &=\int_{x_{1}}^{x_{2}} \int_{y_{1}(x)}^{y_{2}(x)} G(x, y) d x d y   \label{5.17a}\\
G & \equiv \int_{z_{1}(x, y)}^{z_{2}(x, y)} f(x, y, z) d z  \label{5.17b}
\end{align}


最后变成一维的形式


\begin{align}
F=\int_{x_{1}}^{x_{2}} H(x) d x    \label{5.18a}\\
H(x) \equiv \int_{y_{1}(x)}^{y_{2}(x)} G(x, y) d y   \label{5.18b}
\end{align}


可以看出，为了实现这一点，我们需要在每个样本点计算次维积分。

最后，在这种方法中使用高斯正交，结果是

\begin{equation}
F=\sum_{i=1}^{N} \sum_{j=1}^{N} \sum_{k=1}^{N} w_{i} w_{j} w_{k} f\left(x_{i}, y_{j}, z_{k}\right)   \label{5.19}
\end{equation}

\subsubsection{Existing Approaches} 

有几种方法可以在程序中实现积分方法。它们可以使用非常僵硬的结构或非常灵活的动态结构来实现。在这一部分，我们讨论了实现集成方法的不同方法。

第一种，也是最静态的，也是最刚性的，就是在积分函数中手动引入积分坐标和权重。这种方法在有限元程序中被用来在所有积分点创建一个`shape functions`值的数组，或者直接将其引入弱形式，在元素上积分。其优点是积分将非常快，特别是对于低阶简单元素，而所有的值在编译时就已经知道了，所有的循环都可以被消除。缺点是为高阶元素维护这些结构是很困难的，而且这些代码在其他形式或元素中是不可重复使用的。这里有一个例子。

~~~C++
double f(double x, double y)
{
    // function body ...
}
double integral_of_f()
{
    const double x1 = -sqrt(1.00 / 3.00)
                          const double x2 = sqrt(1.00 / 3.00)
                                            // w = 1.00
                                            return f(x1, x1) +
                                            f(x1, x2) +
                                            f(x2, x1) + f(x2, x2);
}

~~~

前面的方法可以通过将一维样本点集和它们的权重封装在一个类中并创建任何$n$维的实例来加强，只是在积分点的维度循环中。通过这种方式，积分点可以在程序的其他部分重复使用，但程序仍然依赖于一维样本点的外积集合。换句话说，这种方式不支持有一个特定的$n$维度的点。


~~~C++
array<double, 2>
    Gauss2 = {-sqrt(1.00 / 3.00),
              sqrt(1.00 / 3.00)};
double f(Point &x)
{
    // function body ...
}
double integral_of_f()
{
    double result = 0;
    const int size = Gauss2.size();
    // w = 1.00
    for (int i = 0; i < size; i++)
        for (int j = 0; j < size; j++)
        {
            Point g(Gauss2[i], Gauss2[j]);
            result += f(g);
        }
    return result;
}
~~~



另一个选择是将正交点的数组及其权重封装在一个正交类中，并为每组的积分点实现这个类的一个实例。这种设计也很常见，相对来说比以前的设计更容易重复使用。其性能在很大程度上取决于这些正交对象的实现和使用方式，但可以像以前的方法一样进行优化。它的弱点是手工编码和调试，特别是当它从一维扩展到二维和三维时。在这种情况下，对于任何集合的高斯积分样本点，1维、2维和3维积分空间的不同正交集必须分别实施和测试。


~~~C++
class Gauss2D2 : public array<Point<2>, 4>
{
public:
    Gauss2D2()
    {
        // Initializing array with
        // integration points
    }
};
double f(Point<2> &x)
{
    // function body ...
}
double integral_of_f()
{
    double result = 0;
    const int size = Gauss2D2 ::size();
    Gauss2D2 gauss_points;
    // w = 1.00
    for (int i = 0; i < size; i++)
        result += f(gauss_points[i]) return result;
}

~~~



对以前设计的一个很好的扩展是，从其一维集合自动构建二维和三维积分点集。这个扩展使我们有了一个新的选择，它的实现更加紧凑，更容易扩展和测试。在Deal II [^21] 中，这种方法被用来实现正交类。正交基类负责将一维集合扩展到它的维度（由模板参数给出）并存储它们，同时为它们提供接口。可扩展性是这个结构的优点。任何新的积分点集合都可以通过从一般基类中派生出来而被添加，任何$n$维度的集合都可以通过提供消减量和权重而被创建，而无需改变库。

\subsubsection{Kratos Quadrature Library Design} 

 Kratos 正交的结构是非常简单和直接的。它由两个类 `IntegrationPoint` 和 `Quadrature` 组成，其中 `set` 类代表样本点阵列，被称为 `IntegrationPoints` 。

该库被分为两部分，一部分是库本身，不允许在扩展方面进行修改；另一部分是扩展，用于添加新的方法以扩展该库。这种分离使得所有的困难都被封装在库的部分，而使扩展部分尽可能的简单。这也是在设计中不使用CRT模式的原因之一。

每个abscise和其相应的权重都封装在`IntegrationPoint`类中。更详细地说，`IntegrationPoint`派生自`Point`类，这使得它可以作为一个点被传递给任何几何函数。另外，`Point`本身也是一个接口，并派生自Array_1d，它物理上包含了坐标值。所有这些使得`IntegrationPoint`可以在需要坐标或点的阵列的地方使用。在Kratos中也有一个相应的权重，在存储、传递和使用它来整合一个函数方面完成了封装水平。 `IntegrationPoint`是一个有3个参数的模板。

- `TDimension` 是一个无符号的整数，表示这个积分点作为一个点的维度，没有权重作为一个额外的维度。

- `TDataType` 是积分点的坐标类型，默认为`double`。

- `TWeightType`是存储在积分点的积分权重类型，默认也是`double`。





 `IntegrationPoints`只是一组特定的的积分点，对应于某些特定的积分算法。它也是该库的扩展点。如果确认了这些条件，任何包含用户指定积分点的`set`的类都可以作为积分点使用。

- 拥有一个在编译时已知的`Dimension`属性。

- 必须提供一个静态的`size()`方法，并且在编译时必须知道大小。

- 还需要有0基数的索引。



 `Quadrature` 是库的引擎。为用户执行界面，也使用拉格朗日乘法器为高维度创建积分点。它可以通过它的三个模板参数进行定制。

- `TQuadraturePointsType` 是创建正交的给定积分点。

- `TDimension` 是一个无符号的整数，代表正交的尺寸，默认情况下是给定积分点的尺寸。

- `TIntegrationPointType` 是用于创建正交的积分点类型。它的默认值是`IntegrationPoint`，有一个给定的尺寸。





\subsection{Linear Solvers} 

解决有限元问题的隐式方法构建了一个代表模型的方程组，并用一个线性求解器来解决它。本节将讨论线性求解器的设计。

\subsubsection{Existing Libraries} 

解算器的独立性以及在许多分析方法中对它们的基本和大量使用，促使一些团体创建解算器库。在Fortran中，有一些成功的库，它们确实已经成熟并被广泛使用。

`Linear Algebra PACKagee` $(L A P A C K)$是一个用于求解线性方程组的经典库。这个库提供了解决同步方程组、特征值和奇异值问题以及线性方程组的最小二乘解的程序。这个库是用Fortran 77编写的，广泛用于fortran有限元代码中。LAPACK95是这个库的一个扩展，它利用Fortran 95的特性提供了更好的接口。LAPACK是为在共享内存`vector`和并行处理器中有效运行而设计的。为了提高可移植性，它使用基本线性代数子程序（BLAS）进行内部操作。因此，通过为任何架构配置BLAS，可以得到相应的优化的LAPACK。缺乏迭代求解器是使用该库解决一些大问题的唯一限制。

`Linear Algebra PACKagee` 中的$C++$，（ $LAPACK ++$ ）是LAPACK的一个 $C++$ 接口。最初的LAPACK++被放弃了，开始了它的继任者`Template Numerical Toolkit`（TNT），它没有达到它的功能。但是最初的$LAPACK++$被另一组开发者改进，现在有了一些额外的功能。$LAPACK++$没有在Kratos中使用，因为在选择求解器的时候，$LAPACK++$正处于被放弃的时期。

在$\mathrm{C}$或$\mathrm{C}++$中也实现了几个求解器，可以直接在C++程序中使用。

`Iterative Template Library`（ITL）提供了一个迭代求解器的集合和一组与它们一起使用的前置条件器。它还提供了一个抽象接口来使用不同的基本线性代数代码，如 `Matrix Template Library` (MTL) 或 `Blitz++` 。由于这个接口，这个库还可以使用不同类型的矩阵和向量。Kratos的第一个版本是用ITL实现的。它清晰的结构和界面以及处理不同类型矩阵的灵活性是它被选中的原因。不幸的是，它的性能在很大程度上取决于编译器的优化器，基本上它在Visual $\mathrm{C}++$编译器上的糟糕表现使我们把它放在一边。

另一个重要的库是`Portable, Extensible Toolkit for Scientific Computation`（PETSc）。它是在BLAS、LAPACK和`Message Passing Interface`（MPI）库的基础上构建的，提供了大量的并行线性和非线性方程的求解器。这个库的优点是性能好，支持分布式机器的并行处理。这个库是用$\mathrm{C}$编写的，在其结构中缺乏$\mathrm{C}++$的特征，无法对其任务进行干净的封装。这个库没有用于实现Kratos，因为当时并行化不是Kratos的目标，而且支持它的单进程计算太复杂了，（特别是在Microsoft Windows下编译和调试这个库是一个复杂的任务）。然而，我们打算在未来支持这个库来实现Kratos的并行化。

\subsubsection{Kratos' Linear Solvers Library} 

在Kratos中，实现了一个线性求解器库，以便有一组小而有效的求解器可以使用，而不需要链接一些外部求解器。这个库被设计为独立于程序的其他部分，计算效率高，使用的矩阵和向量类型灵活。

三组的类组成了线性求解器库。解算器封装了解算算法。先决条件器被求解器使用，并修改方程系统以获得更好的收敛性，最后执行逆过程以获得结果。重排器改变方程的顺序，以减少带宽或更好地使用缓存，也为结果做反置换。

\textbf{Spaces For Encapsulating Operations} 

使用线性求解器或加入一个新的线性求解器的主要问题之一是它们使用的矩阵和向量类型可能与应用程序使用的类型不兼容。为了解决这个问题，需要另一个抽象层来封装矩阵和向量操作，并作为新矩阵和向量类型的接口。

这里使用的思想与LAPACK和ITL中使用的相同，但有一些修改。如前所述，LAPACK实现了求解器的算法，并使用BLAS进行代数操作。这种结构使得它可以为不同的机器进行配置。ITL也使用了某种程度上类似的设计。它有一组的函数定义在它的命名空间中，用户可以通过重载来实现对其矩阵和向量的操作。虽然ITL使用模板，但它也可以接受不同类型的矩阵，并通过用户提供的自定义操作来操作它们。

LAPACK方法的问题是矩阵和向量的类型不能被改变。对于ITL来说，这个问题用模板解决了，但是接口并没有提供不同类型的矩阵和向量的完全分离。例如，两个开发者可以通过重载两种不同类型的矩阵的接口方法来实现两个接口，但使用相同的 `vector` 。在这种情况下，对向量的操作在两个接口中都是一样的，会导致冲突。

在Kratos中，这个想法通过定义`Space`作为一个新概念得到了改进。 `Space`定义了一个矩阵和一个`vector`以及它们的运算符。将定义和运算符封装在一个对象中，可以让不同的开发者实现他们的接口而没有任何冲突。 `Space`定义了一个简单的接口，包含线性求解器所需的所有操作。[表5.1]显示了这个接口。表中$A$和$B$是矩阵，$X, Y$和$Z$是向量，$\alpha$和$\beta$是标度常数。


值得一提的是，并非所有`Space`中定义的方法都必须为所有用户定义的空间实现。我们可以根据解算器的需要实现其中足够的`set`。

\textbf{Linear Solvers} 

线性求解器的第一个要求是可以相互交换。虽然没有适用于所有情况的最佳求解器，而且一般来说，选择适当的求解器也取决于它所要解决的问题，但用户非常希望能够从一个求解器转换到另一个，以便找到适用于他们情况的最佳求解器。这个功能可以用 `Strategy Patterns` 提供。将这种模式应用于线性求解器的结果是图中所示的结构[5.5] 。





使用这种模式，每个 `LinearSolver` 分别封装了一种求解算法，也使它们可以互换。这样，增加一个新的求解算法就相当于增加一个新的 `LinearSolver` 。这种封装使该库更容易扩展。 `User`保持一个指向`LinearSolver`基类的指针，可以指向求解器家族的任何成员，并使用基类的接口来调用不同的程序。

这种结构可以通过将其划分为两个分支来改进，即直接求解器和迭代求解器。图[5.6]显示了这种结构。将这两类分开，可以为它们添加特殊接口。

`IterativeSolver`类存储公差，迭代次数和最大迭代次数。它还提供了控制收敛和所需迭代的方法。`DirectSolver`只是一个抽象层，没有给它的基类添加任何新功能。

线性求解器可以使用重排器来减少矩阵的带宽，这对使用直接求解器的求解成本很重要。迭代求解器也可以使用重排器来改善缓存内存的使用。我们的设计必须提供一种简单的方法来添加新的`reordering`算法，也让它们与任何线性求解器相结合。

 这里的`Strategy Patterns`是用来将每个`reordering`算法封装在一个具有标准接口的类中。通过这种方式，保证了可扩展性和可互换性。图[5.7]显示了这种结构。

桥接模式被用来连接线性求解器和重订器。使用这种模式，用户可以毫无问题地将每个再orderers与任何线性求解器结合起来。图[5.8]显示了组合后的结构。

下一步是设计前置条件器。它们被迭代求解器用来增强解的收敛性。它们的结构必须允许开发者轻松添加新的`preconditioning`算法。也有必要让用户在没有问题的情况下将一个先决条件器换成另一个。在设计这个结构时使用了一种策略模式。通过这种方式，每个`preconditioning`算法都被封装在一个对象中，以后可以独立添加到程序的其他部分。同时，由`Preconditioner`基类建立的统一接口允许用户毫无问题地用另一种算法改变一种算法。图[5.9]显示了这种结构。

如前所述，迭代求解器使用了预处理程序。一个`bridge`模式被用来连接这两个结构，使任何迭代求解器与任何预处理器的结合成为可能。图[5.10]显示了使用`bridge`模式的组合结构。

所有这些类将两个空间作为其模板参数。一个稀疏空间用于定义对稀疏矩阵和向量的操作，一个密集空间用于定义对密集矩阵和向量的操作，这些矩阵和向量被用作辅助矩阵和向量或用于提供多个右手边。为了解决带有密集矩阵的小方程，人们可以用两个密集空间创建这些类，而没有问题。

\subsubsection{Examples} 

这里以一个简单的共轭梯度求解器为例，说明如何在一个给定的空间上实现一个算法。

~~~C++
template <class TSparseSpaceType,
          class TDenseSpaceType,
          class TPreconditionerType class TReordererType>
class CGSolver : public IterativeSolver<TSparseSpaceType,
                                        TDenseSpaceType,
                                        TPreconditionerType,
                                        TReordererType>
{
public:
    CGSolver(double NewTolerance,
             unsigned int NewMaxIterationsNumber)
        : BaseType(NewTolerance, NewMaxIterationsNumber) {}
    CGSolver(double NewMaxTolerance,
             unsigned int NewMaxIterationsNumber,
             PreconditionerPointerType pNewPreconditioner)
        : BaseType(NewMaxTolerance,
                   NewMaxIterationsNumber,
                   pNewPreconditioner) {}
    virtual ~CGSolver() {}
    bool Solve(SparseMatrixType &rA,
               VectorType &rX,
               VectorType &rB)
    {
        if (IsNotConsistent(rA, rX, rB))
            return false;
        GetPreconditioner()->Initialize(rA, rX, rB);
        GetPreconditioner()->ApplyInverseRight(rX);
        GetPreconditioner()->ApplyLeft(rB);
        bool is_solved = IterativeSolve(rA, rX, rB);
        GetPreconditioner()->Finalize(rX);
        return is_solved;
    }

private:
    bool IterativeSolve(SparseMatrixType &rA,
                        VectorType &rX,
                        VectorType &rB)
    {
        const int size = TSparseSpaceType ::Size(rX);
        mIterationsNumber = 0;
        VectorType r(size);
        PreconditionedMult(rA, rX, r);
        TSparseSpaceType ::ScaleAndAdd(1.00, rB, -1.00, r);
        mBNorm = TSparseSpaceType ::TwoNorm(rB);
        VectorType p(r);
        VectorType q(size);
        double roh0 = TSparseSpaceType ::Dot(r, r);
        double roh1 = roh0;
        double beta = 0;
        if (roh0 == 0.00)
            return false;
        do
        {
            PreconditionedMult(rA, p, q);
            double pq = TSparseSpaceType ::Dot(p, q);
            if (pq == 0.00)
                break;
            double alpha = roh0 / pq;
            TSparseSpaceType ::ScaleAndAdd(alpha, p, 1.00, rX);
            TSparseSpaceType ::ScaleAndAdd(-alpha, q, 1.00, r);
            roh1 = TSparseSpaceType ::Dot(r, r);
            beta = (roh1 / roh0);
            TSparseSpaceType ::ScaleAndAdd(1.00, r, beta, p);
            roh0 = roh1;
            mResidualNorm = sqrt(roh1);
            mIterationsNumber++;
        } while (IterationNeeded() && (roh0 != 0.00));
        return IsConverged();
    }
}; // Class CGSolver

~~~





















\subsection{Geometry} 

使用有限元方法解决一个问题需要对模型进行离散化，这包括将模型划分为几个具有定义形状的小分区。几何图形被设计用来封装这些形状，管理它们的数据，并计算它们的参数。本节介绍了Kratos中几何体的设计和实现。

\subsubsection{Defining the Geometry} 

如$3.2$节所述，在有限元中，偏微分方程被转换为离散空间中的等价物`integral form`，其具有一般形式。

\begin{equation}
\int_{\Omega} L_{1}(N a) d \Omega+\int_{\Gamma} L_{2}(N a) d \Gamma=0
\end{equation}

其中$N$是形状函数，$a$是插值参数。这个`integral form`被划分为域和边界上的子积分，其结果是以下元素形式。

\begin{equation}
\sum_{e}^{n_{e}} \int_{\Omega_{e}} L_{1}(N a) d \Omega_{e}+\sum_{c}^{n_{c}} \int_{\Gamma_{c}} L_{2}(N a) d \Gamma_{c}=0
\end{equation}

其中$n_{e}$是元素的数量，$n_{c}$是条件的数量。这种形式由元素或边界域上的积分组成。`shape functions` $N$的值和它们的导数对于评估积分是必要的，因为算子$L_{1}$和$L_{2}$通常有导数算子。`shape functions`的导数是通过`jacobian matrix`$\mathbf{J}$的逆运算来计算的，在以下三维空间的方程式中可以看到。

\begin{equation}
\begin{aligned}
&{\left[\begin{array}{c}
\frac{\partial N_{i}}{\partial \xi} \\
\frac{\partial N_{i}}{\partial \eta} \\
\frac{\partial N_{i}}{\partial \zeta}
\end{array}\right]=\left[\begin{array}{ccc}
\frac{\partial x}{\partial \xi} & \frac{\partial y}{\partial \xi} & \frac{\partial z}{\partial \xi} \\
\frac{\partial x}{\partial \eta} & \frac{\partial y}{\partial \eta} & \frac{\partial z}{\partial \eta} \\
\frac{\partial x}{\partial \zeta} & \frac{\partial y}{\partial \zeta} & \frac{\partial z}{\partial \zeta}
\end{array}\right]\left[\begin{array}{l}
\frac{\partial N_{i}}{\partial x} \\
\frac{\partial N_{i}}{\partial y} \\
\frac{\partial N_{i}}{\partial z}
\end{array}\right]=\mathbf{J}\left[\begin{array}{c}
\frac{\partial N_{i}}{\partial x} \\
\frac{\partial N_{i}}{\partial y} \\
\frac{\partial N_{i}}{\partial z}
\end{array}\right]} \\
&{\left[\begin{array}{c}
\frac{\partial N_{i}}{\partial x} \\
\frac{\partial N_{i}}{\partial y} \\
\frac{\partial N_{i}}{\partial z}
\end{array}\right]=\mathbf{J}^{-1}\left[\begin{array}{c}
\frac{\partial N_{i}}{\partial \xi} \\
\frac{\partial N_{i}}{\partial \eta} \\
\frac{\partial N_{i}}{\partial \zeta}
\end{array}\right]}
\end{aligned}
\end{equation}

为了进行整合，一种方法是将全局坐标变为局部坐标。这种转换需要计算`jacobian matrix` $J$及其行列式。所有这些过程的描述都是为了定义几何体，并帮助我们确定什么可以被几何体所封装。

 `Geometry`封装了所有的几何信息，如其尺寸、点、边以及一些辅助操作，如计算中心点、面积、体积等。除了这些几何信息，它还提供积分点和`shape functions`的值以及它们的局部和整体导数。它还计算`jacobian matrix`$\mathbf{J}$，jacobian的逆$\mathbf{J}^{-1}$和它的行列式$|\mathbf{J}|$。

所有这些操作都被封装起来，使元素开发者能够以通用的形式编写他们的`Elements`，并与几何学的类型无关。

\subsubsection{Geometry Requirements} 

首先， 所有的`Geometry`都必须是非常轻量级的，并具有内存效率。这一要求来自于这样一个事实：对一个实际问题进行建模通常需要在内存中存储大量的几何体并对其进行处理。对每个几何体有任何不必要的开销，甚至不需要的开销，都会导致程序使用大量的内存开销。

 `Geometry`其操作也必须是快速的。 `Elements`在代码的大多数内循环中使用几何体进行计算，因此它们的性能会严重影响代码的整体性能，特别是在建立方程系统的时候。这样一来，所有可以计算一次的参数都必须保留，以便在使用时不需要重新计算。另外，接口和算法的设计和实现必须使临时性的创建达到最小。

另一个要求是使用户能够将一个通用的`Element`与任何`Geometry`结合起来，而不改变它。为了实现这个目标，几何结构必须为所有的几何体提供一个标准接口。这样一来，从一个几何体到另一个几何体的改变就会很容易，而使用的接口也是一样的。

最后，添加一个新的几何体必须在不需要改变其他部分代码的情况下完成。一个良好的封装和与其他部分代码的清晰连接可以帮助实现这一目标。

\subsubsection{Designing Geometry} 

与线性求解器的结构一样，在几何结构中也使用了一种策略模式。使用这种模式使得它们可以通过一个统一的接口进行互换。它还将所有`Geometry`数据和操作封装在一个对象中，这使得它们彼此之间更加独立，更容易被扩展。图[5.11]显示了这种结构。





复合模式也可以用来让用户组合不同的几何体，形成一个复杂的几何体。这种结构在某些需要定义复杂几何体的情况下可能很有用。图[5.12]显示了建议的结构。





有不同的方法来实现几何体。一些常见的方法是将它们定义为点和构造规则的集合。另一种方式是一种层次结构形式，其中一个三维形状由其二维的边缘定义。这些边就像其他二维几何体一样，由其一维的边来定义。最后，这些边是由点来定义的。以这种形式实现的几何体为用户提供了完整的信息集合，在需要关于几何体及其边的详细信息的情况下是非常有用的。

在Kratos中，这种实现方式被认为过于复杂，因此采用了第一种方法。这个决定有两个主要原因。在Kratos中实现的算法与几何体的点一起工作（`Nodes`用于`Element`几何体）。由于这个原因，对`Nodes`的快速访问比对它的边更重要。第二个原因是保存所有层次的内存开销相当大，对于通常的元素算法来说是多余的。所有这些使我们使用第一种方法，但在几何学中保留一个空位来插入边的数据库。 `Geometry`是来自于点阵列的。这种方法使用户可以像操作一个点阵列一样操作几何体。例如，通过在一个循环中移动所有的点来移动它，或者使用其点迭代器来应用$\mathrm{C}++$标准库。




`Geometry`的接口反映了它所提供的所有操作。[表5.2]显示了实现几何操作的方法，[表5.3]显示了提供有限元操作的方法。

为了优化`Geometry`的性能，所有产生`vector`、矩阵或张量形式的结果的接口都接受其结果作为附加参数。下面是一个例子。

~~~C++
Matrix &Jacobian(Matrix &rResult,
                 IndexType IntegrationPointIndex) const
{
    // Calculating the jacobian ...
    return rResult;
}

~~~


另一种设计是获取参数作为参数并返回计算结果。这里是使用这种替代设计的前一个例子。

~~~C++
Matrix Jacobian(IndexType IntegrationPointIndex) const
{
    Matrix result;
    // Calculating the jacobian ...
    return result;
}
~~~


这个替代方案似乎更优雅，但由于创建了几个不必要的临时变量，在`Geometry`性能上产生了很大的开销。

由`Geometry`提供的信息可以分为两个不同的类别。




常量信息，它只取决于几何体的类型，对所有具有相同类型的几何体都是一样的。 `Dimension`、积分点、`shape functions`值和`shape functions`局部梯度是这一类别的操作实例。

非常数 第二类包含其余的信息，这些信息可以从一个几何体改变到另一个几何体。例如，点、雅各布和`shape functions`梯度都属于这一类。

为了进一步优化几何体的性能，必须考虑这两个类别的不同性质。 首先，常数信息可以计算一次并储存起来，以便在必要时使用。同时，也不需要使用虚拟方法来访问这些信息。虚拟函数会因为其函数调用延迟而降低性能，特别是对于像返回值这样的小操作的方法。因此，让这些方法不再是虚拟的可以提高它们的性能。

正如前面提到的，常量信息可以被计算一次并存储起来以后使用。这个想法的缺点是指向每个几何体的这些数据的指针所需的内存。虽然这些指针所使用的内存相对较小，但创建大量的几何体却使其变得相当大。解决这个问题的一个好办法是创建一个辅助对象，把所有的常量信息放在里面。这样一来，每个几何体中只有一个指针需要指向这个对象并访问所有常量信息。 `GeometryData`被定义和实现，以保存几何体中的所有常量信息。图[5.13]显示了这个类和它的属性。

 `Geometry`保持对几何数据的引用，并使用它来提供常量信息，它没有虚拟方法。图[5.14]显示了完整的结构。

对于每种类型的几何体，都会创建一个`GeometryData`的静态变量，并在构造时将其引用给几何体。






\section{Variable Base Interface} 

\subsection{Introduction} 

在有限`Element`方法的发展过程中，有限元代码发生了很大的变化。在其早期，有限元的应用是为了解决特定领域中的某类问题而开发的。许多用于解决壳、板、斯托克斯流体等问题的应用都是在这个时期产生的。到了后来，工程师们开始将有限元应用于工程问题，这些问题大多由一种以上的类型组成。 首先，他们试图通过分离问题并通过一个模块解决每个部分的问题，同时开发人员开始将以前与同一领域有关的小模块聚集在一个特定领域的应用程序中，从而可以处理更复杂的问题。沿着这条路线，各种用于解决结构、流体和其他问题的应用程序应运而生。

应用有限元来解决`coupled problem`是一个自然的步骤，从单一领域的问题。重用现有的代码并通过接口连接它们是处理这些问题的最常见和最认可的方式。通过这种方式，复杂性降低了，而且解决每个领域的现有模块可以被重新使用来解决 `coupled problem` 。但是如果接口不一致会怎么样呢？写一个文件并在另一个模块中读取它是一种常见的方式，而有一种更优雅的方式是使用可以以通用方式处理对象的库。有许多成功的例子使用文件接口或库，如 CORBA [^101] 或 omniORB，尽管使用它们会在代码的性能上造成巨大的开销。在CORBA对象上调用一个请求的延迟时间大约是 $500-5000$ 的两倍，比在 $\mathrm{C}++$ [^58] 中通过一个函数调用来做要高。

在这些日子里，开发和扩展有限元应用程序以解决新的和更复杂的问题是一个挑战。同时，在任何新的领域使用有限元也会产生新的需求，需要开发一个应用程序来使该方法在实践中得到应用。因此，灵活性、可扩展性和可重用性是现代有限元代码设计和发展的关键特征。

尽管以前在一个应用程序中把不同的模块串联起来的策略很好，但可重用性的水平却分别很低。原因是，在这种方式下，我们可以重用整个模块，但不能重用其中的一部分。例如，每个模块都有自己的数据结构和IO例程，不能被另一个方法或新模块以同样的方式使用。这就是建立一个可以在高层和低层以同样方式使用的变量基础接口的动机。

\subsection{The Variable Base Interface Definition} 

在有限元程序的不同部分之间的许多连接点中，我们要询问一些变量的值或映射一些变量，等等。设计这种接口的方法是将每个请求与它所依赖的一个或多个变量一起发送。这听起来很简单，也很自然，同时这也是使用变量来制作通用算法和模块的一个强大的技巧。

 首先，让我们看看我们一般需要什么来创建一些有限元通用算法。

- 变量的类型是一个重要的信息，许多操作和存储机制都取决于它。

- 有时我们也需要变量的名称，（即打印结果）。

- 虽然每个变量都可以通过它的名字来识别，但是在检查点中使用名字会产生很大的不必要的开销（比较两个字符串是很慢的），所以一个标识符号是必不可少的，例如在快速搜索和数据库工作的情况下。

- 另一个有用的信息是一个零值。虽然这个信息看起来没有必要，但它对初始化零向量和矩阵的正确大小有很大的帮助。

- 此外，在某些模块中，我们可能只需要处理一个变量的某些组成部分。在这种情况下，我们需要一种机制来识别组件的所有者。

- 另一个有用的功能是与原始指针一起工作。这在连接外部模块和提取模块传递的指针值的情况下特别有用。

考虑到上述所有要求，现在可以进行全局设计了。在这个变量基础接口中。

- 一个变量封装了不同对象所需的所有信息，以便在不同的变量上以通用方式工作。这样做有助于简化模块的接口。考虑到一个`PrintNodalResult`模块，它通常需要结果名称，检索节点结果的索引，以及如果某些`Nodes`没有结果的零值。在这个设计中，所有这些信息都可以通过一个变量传递。

- 每个模块必须根据给它的变量来配置自己。

- 通过静态地将变量类型化来达到类型安全。

- 变量类的每个实例的名称与它所代表的变量名称相同。这对代码的可读性是一个很大的附加值。

\subsection{Where Can be used?} 

如前所述，在一个多学科的应用中，总是需要在不同领域之间交换数据。因此，重要的一点是要以一种稳健和安全的方式交换数据。通过VBI（`Variable`基础接口）开发不同的模块，使它们之间的数据交换变得非常简单，尽管每个领域有不同的数据存储，有时内部数据结构可能是不同的。例如，流体应用可能有速度和压力的存储，而热力领域只有温度。当使用变量时，接口提供了恢复其值以及为给定变量存储新值的方法。





现在让我们再往下走一级，以符合VBI的方式开发一组的通用算法和数据结构。这可以提高代码的可重用性。同时，代码管理变得更容易，而任何新的扩展都可以在不改变接口的情况下完成。有几种算法可以配备VBI。比如说。


\textbf{Data Structure} 最重要的地方是在数据结构中存储和检索一个值。这里需要类型、id和零值来创建一个类型安全的数据容器。所有这些信息都由一个变量来表示。接口是`GetVAlue`或`SetValue`，以此类推，给定变量。

\textbf{Reading input file} 每个变量都有其类型和名称，所以它们可以用来验证 `tokens` 。通过这种方式，输入解释器可以扩展到读取新的数据，只需定义一个呈现这种新数据的变量。

\textbf{Printing output} 打印输出需要变量的名称和它的值，在没有数据的情况下，需要零。使用VBI的数据结构，并发送一个打印变量的请求，可以得到所有的信息，即使是新的变量也可以打印。

\textbf{Mapping or interpolating} 再次使用符合VBI的数据结构，我们可以概括，或者`map`和插值算法。

\subsection{Kratos variable base interface implementation} 

在Kratos中，VariableData及其衍生物`Variable`和`VariableComponent`代表接口信息。

\subsubsection{`VariableData`} 

 `VariableData` 是基类，它包含了关于它所代表的变量的两个基本信息；它的名字 `mName` ，和它的关键数字 `mKey` 。

 `VariableData` 对这两个属性有琐碎的访问方法，同时也有虚拟和空的方法用于原始指针操作。这里没有实现这些方法的原因是，`VariableData`没有任何关于它所代表的变量类型的信息。在`VariableData`中缺乏类型信息使得它不适合在接口的许多部分传递信息，但这个类的想法是提供一个较低层次的信息，这在所有类型的变量和它们的组件之间是通用的，当变量和它们的组件之间没有区别时，用它作为一个存放者。另外，在这个实现中，我们使用了一个虚拟方法库来调度对原始指针的各种操作。这在某些情况下可能会导致性能不佳，而函数调用的开销似乎相当大。

\subsubsection{Variable} 

 `Variable`是这个结构中最重要的类。它有由`VariableData`代表的信息，而`VariableData`是由它派生的。此外，它还有另一个重要的信息，即代表变量的类型。

使用C++作为实现语言`Variable`，它的数据类型是一个模板参数。通过这种方式，接口可以为每种类型的数据进行特殊化，也可以实现类型安全。另一个重要的好处是，在模板形式下的变量，其数据类型作为模板参数，在接口中可以有一个限制的形式。例如，如果我们想限制一个方法只接受矩阵，那么通过传递一个`variable<Matrix>`作为它的参数，我们可以防止用户错误地传递其他类型。这个特点在某些情况下是很重要的，因为有不同的类型代表同一个变量。(构成矩阵及其相应的`vector`就是这种情况的一个很好的例子）。)  `Variable` 数据类型`TDataType`必须满足以下要求。

- 可复制构建。

- 可分配的。

- 定义的流操作符

在 `Variable` 中，通过了解数据类型，原始指针操作方法被实现。这些方法使用原始指针来对其类型进行基本操作。

- `Clone` 使用类的复制构造函数创建此对象的副本。它需要一个void*作为参数，并返回指向为克隆对象分配的内存的void*。 `Clone`对于避免复杂对象的浅层复制是很有用的，而且也不需要实际掌握变量类型的信息。

- 复制与`Clone`非常相似，除了目标指针也作为一个void*参数传递给它，它是一个有用的方法，特别是创建异质数据阵列的副本。

- Assign与Copy非常相似。它只是在复制构造函数旁边使用了赋值运算符。Copy是创建一个新的对象，而Assign是对两个现有对象进行赋值。

- `AssignZero`是Assign的一个特例，它使用变量零值作为源。这个方法对于初始化数组或重设内存中的值很有用。

- 删除将一个变量类型的对象从内存中删除。它的参数是一个void*，即要删除的对象的位置。Delete调用对象的析构器以防止内存泄漏，并释放分配给该对象的内存，假设该对象分配在堆中。- Destruct是为了销毁一个对象，保持它所使用的内存。它需要一个void*作为参数，这个参数是要被销毁的对象的位置。它调用对象的析构器，但与Delete不同，它对分配给它的内存不做任何处理。所以它在重新分配一部分内存的情况下非常有用。

- 打印是一个辅助方法，用于产生知道其地址的给定变量的输出。例如，使用这个方法可以在输出流中写入一个异质的容器。这个方法假定为变量类型定义了流操作符。

所有这些方法都可以用于低级别的使用。它们很有用，因为它们可以被一个`VariableData`的指针调用，并且同样适用于所有安排在内存中的数据类型，但是使用这些方法维持类型安全并不简单，需要特别注意。

零值是`Variable`的另一个属性，存储在mZero中。当一个通用算法需要在不失去通用性的情况下初始化一个变量时，这个值特别重要。例如，一个计算某个`Elements`的变量的规范的算法，如果根本没有`Element`，就必须返回一个零。在`double`值的情况下，调用变量类型的默认构造函数是没有问题的，但是对`vector`或矩阵值应用同样的算法会导致问题，因为这种类型的默认构造函数将没有正确的大小。但是返回一个零值而不是默认的构造值可以保持算法的通用性，即使是对向量和矩阵，假设变量定义正确的话。

还有一个方法是StaticObject。这个方法只是返回None，它是这个类型的静态变量，名字是None，可以用于未定义变量的情况（像指针的null）。它只是一个辅助变量，帮助管理未定义的变量，没有初始化或特殊情况。

\subsubsection{VariableComponent} 

如前所述，有些情况下，我们只想处理一个变量的组成部分，而不是全部。 `VariableComponent` 实现了对这些情况的帮助。

 `VariableComponent` 像`Variable`一样源自`VariableData` 。

 `VariableData` 是一个以适配器为参数的模板。适配器是组件机制的延伸点。对于任何新类型的组件，都需要实现一个新的适配器。这个适配器类型的要求是。

- GetSourceVariable方法来检索父变量。

- `GetValue`方法用于转换从源变量值中提取的组件值。

- StaticObjec用于创建无组件。

与`Variable`不同，`VariableComponent`没有实现零值或原始指针操纵器。零值可以从源值中提取，所以这里没有必要有。对原始指针的操作在这里是不允许的。这个接口完全管理变量，而不是仅仅管理变量的某些部分。事实上，一个对象的一部分不能被复制、克隆、删除或销毁。所以这些方法的实现不是为了保护对象免受不安全的内存操作。

将适配器作为模板参数有助于编译器优化代码和消除开销。在这种方式下，适配器的`GetValue`方法将被内联到`VariableComponent`的方法中，所以不会有任何由于分解而产生的开销，同时达到可扩展性。





\subsection{How to Implement Interface} 

用变量发送任何请求是这个接口的工作方式。但是为了使事情在现实世界中运行，必须定义一个统一的方法来获取和设置对象中的变量值。

\subsubsection{Getting Values} 

在许多接口中都有一些方法，比如`GetDisplacement`或`Flux`来获取例如位移或热流量的值。这对于一个通用接口来说是行不通的，因为要访问的变量在不同的领域可能完全不同。我们的想法是不通过方法名来指定变量，而是通过传递给通用方法的变量。通过这种方式，任何先前定义的算法都可以通过这种通用访问方法被重新用于新领域和新变量。

拥有一个`GetValue`方法对于每个包含要处理的数据的类来说是必不可少的。请注意，这个方法的名字并不随我们想要得到的变量而改变。 对于只有一种类型或少数类型数据的类，它可以被写成一个使用特定变量的普通方法

~~~C++
DataType GetValue(VariableType const &) const
    
DataType const &GetValue(VariableType const &) const
    
DataType &GetValue(VariableType const &)
~~~



这三个版本所返回的值适用于不同的条件。


第一个版本更适合于返回一些计算出来的、没有存储的值，也适合于像双数这样的小对象。

第二个版本适用于内部值访问方法，只返回对内部变量的引用，没有任何费用。

第三个版本是通过这个接口对一个内部变量进行左值表示。返回的引用不是常量，所以值可以改变，可以作为左值使用。

在包含任意类型变量的类的情况下，如异质容器`GetValue`可以以模板形式实现，保持代码的通用性。

~~~C++
template <TDataType > TDataType GetValue(Variable <TDataType > const &)
const

template <TDataType > TDataType const& GetValue(Variable <TDataType >
const &) const

template <TDataType > TDataType& GetValue(Variable <TDataType >
const &)
~~~


通过这种方式，接口不仅对任何新的变量开放，而且对任何新的变量类型开放。在这里，模板成员的专业化可以作为一个非常有用的工具来定义特殊情况，也可以使用不同的专业化实现来优化代码。

有时，有必要取一个变量组件，而不是孔变量。请注意，`VariableComponent`不能转换为`Variable`，因此不能在上述模式中作为参数传递。所以为了实现这种可能性，需要实现另一个以`VariableComponent`为参数的`set`的方法。和以前一样，具体的实现可以使用已知的适配器来完成，用于特定的组件。

~~~C++
DataType GetValue(VariableComponentType const &) const

DataType const& GetValue(VariableComponentType const &) const

DataType& GetValue(VariableComponentType const &)
~~~



也可以用模板实现更多的通用接口。

~~~C++
template <TAdaptorType > typename TAdaptorType ::Type GetValue(VariableComponent <TAdaptorType > const&) const

template <TAdaptorType > typename TAdaptorType ::Type const&
GetValue(VariableComponent <TAdaptorType > const&) const

template <TAdaptorType > typename TAdaptorType ::Type& GetValue(VariableComponent <TAdaptorType > const &)
~~~





在上述模式中，在适配器中定义了组件的类型，用于指定返回值的类型。通过这种方式，适配器负责指定返回值的类型并保证可扩展性。

最后，获取变量的方法和获取组件的方法可以合并为一组的方法，形成一个统一的接口。

~~~C++
template <TVariableType > typename TVariableType :: Type GetValue(TVariableType const &) const

template <TVariableType > typename TVariableType :: Type const& GetValue(TVariableType const &) const

template <TVariableType > typename TVariableType :: Type& GetValue( TVariableType const &)
~~~


\subsubsection{Setting Values} 

这一部分接口具有与获取值相同的特性。虽然第三个版本的获取值可以用来设置为左值，但在某些情况下需要一个单独的`set`方法来保证接口的通用性。

为了设置一个变量的值，有必要同时传递变量和它的值作为`SetValue`参数。同样，实现方式取决于必须通过该接口访问的各种变量。对于少数变量类型，访问`SetValue`方法可以通过了解数据类型和变量类型来实现。

~~~C++
void SetValue(VariableType const&, DataType const &)
~~~


不是说在这里我们只有一个版本，因为`SetValue`方法不能是常量。同样，为了实现一个更普遍的版本，模板是有用的。

~~~C++
template <TDataType > void SetValue(VariableType const&, TDataType const &)
~~~


在设置组件的情况下，上述两个版本必须被修改，以便接受`VariableComponent`而不是`Variable` 。
~~~C++
void SetValue(VariableComponentType const&, DataType const &)
~~~

或者使用adaptor模版:
~~~C++
template <TAdaptorType>
void SetValue(VariableComponentType const &,
              typename TAdaptorType ::Type const &)
~~~

最后，统一的版本将变量和它们的组件结合在同一方法中。

~~~C++
template <TVariableType>
void SetValue(TVariableType const &,
              typename TVariableType ::Type const &)
~~~

\subsubsection{Extended Setting and Getting Values} 

在某些情况下，需要额外的信息来访问一个类中的变量。在简单的情况下，一个额外的索引就足以表明解决方案的步骤或`Node`的数量，这些都作为额外的参数传递。

在更复杂的接口中，一个具有VBI接口的信息对象也被作为参数传递，以扩展其通用性。这样一来，任何新的信息都会通过现有的接口传递，并且可以被接口检索到。

~~~C++
template <TVariableType> typename TVariableType ::Type
GetValue(TVariableType const &,
         InfoType const &) const

template <TVariableType> typename TVariableType ::Type const &
GetValue(TVariableType const &,
         InfoType const &) const

template <TVariableType> void SetValue(TVariableType const &,
typename TVariableType ::Type const &,
                  InfoType const &)

~~~



使用变量基接口来扩展自己是一个非常强大的技巧，可以在不牺牲代码的清晰度的情况下创建极其灵活的接口。这样一来，不仅要传递的类型和变量可以扩展，而且要传递的参数的数量和类型也可以任意扩展。

\subsubsection{Inquiries and Information} 

并非所有时候我们都需要通过接口来传输数据，有些情况下需要进行查询，或者需要关于某个对象的某个变量的某些信息。假设在这种操作中，变量的类型和成分并不重要，基类`VariableData`可以用在接口中，以同样的方式处理所有种类的变量及其成分。

\subsubsection{Generic Algorithms} 

现在是使用这个接口实现通用算法的时候了。一般来说，算法接口可以使用上面描述的任何形式来获取和设置，特别是扩展形式。事实上，每一种算法在接受的参数上都有很大的不同，但这里重要的是通过接口传递的所需的一个或多个变量。重点是在算法中使用对象的接口，使用传递给它的变量。这样一来，算法对工作变量是透明的，可以普遍应用于新领域的任何新变量。

许多算法都可以用这种方式来概括，读取输入，打印结果，映射和插值，计算规范和误差，等等。所有这些算法都有一个共同的属性：它们对一些变量的值进行操作。仔细实现这些算法可以使它们免于真正知道它们在对哪个变量进行操作。他们只是在用户传递给他们的变量上工作，并使用该变量来提取所需的值。换句话说，算法是在两层接口之间实现的。

这个想法看起来很简单，但在实践中会产生可重复使用的算法，这些算法可以在任何新的工作领域使用新的变量集合。

\subsection{Examples} 

为了了解上述想法在实践中是如何运作的，也为了使一些细节更加清晰，接下来将介绍一些来自不同层次界面的例子。

\subsubsection{Nodal interface} 

第一个例子是访问有限元程序中的结点值。我们将考虑，任何新的程序扩展都可能引入新的的变量集和来访问。在这种情况下，使用变量基础接口可以解决所需的扩展性。一个基本的获取和设置值接口的实现是为了提供基本的可访问性，如下所示。


~~~C++
// Getting a reference
template <class TVariableType>
typename TVariableType ::Type &
Node::GetValue(TVariableType const &)
{
    // Accessing to database and
    // returning value ...
}
// Getting a constant reference
template <class TVariableType>
typename TVariableType ::Type const &
Node::GetValue(TVariableType const &) const
{
    // Accessing to database and
    // returning value ...
}
template <class TVariableType>
void Node::SetValue(TVariableType const &, typename TVariableType ::Type const &)
{
    // Accessing to database and
    // setting value ...
}

~~~


可以看出，统一版本的接口完美地适应了这里，避免了我们对变量和变量组件使用不同版本的`GetValue`和`SetValue`。最后重写[]运算符使语法更容易使用。

~~~C++
template <class TVariableType>
typename TVariableType::Type &
Node::operator[](const TVariableType &)
{
    return GetValue(rThisVariable);
}
~~~


同时可以实现一些查询，看看变量是否存在。

~~~C++
template <class TDataType>
bool Node ::Has(Variable<TDataType> const &) const
{
    // Check if the variable
    // stored before ...
}

template <class TAdaptorType>
bool Node ::Has(VariableComponent<TAdaptorType> const &) const
{
    // Check if the variable component
    // stored before ...
}
~~~






在这里，用于实现这部分接口的单独的变量和组件方法。如前所述，只要在接口中使用`VariableData`就可以做到这一点。

~~~C++
bool Node ::Has(VariableData const &) const
{
    // Check if the variable
    // stored before ...
}
~~~


注意，要使用`VariableData`版本，需要从内部容器中按键ID进行统一搜索。

现在很容易在代码中使用这个`Node`，通过接口访问任何变量。

~~~C++
// Getting pressure of the center node
double pressure = center_node[PRESSURE];
// Setting velocity of node 1
Nodes[1][VELOCITY] = calculated_velocity;
// Printing temperature of the
// nodes to output
for (IteratorType i_node = mNodes.begin();
     i_node != mNodes.end(); i_node++)
{
    std::cout << "Temperature of node #"
              << i_node->Id()
              << " = "
              << i_node->GetValue(TEMPERATURE)
              << std::endl;
}
// Setting displacement of nodes to zero
Vector zero = ZeroVector(3);
for (IteratorType i_node =
         mNodes.begin();
     i_node != mNodes.end(); i_node++)
{
    i_node->SetValue(DISPLACEMENT, zero);
}

~~~




访问节点变量的历史是一个简单扩展接口的例子。保留之前`GetValue`和`SetValue`的参数，可以传递一个额外的参数来指示例如需要的时间步长。

~~~C++
template <class TVariableType>
typename TVariableType ::Type &
Node ::GetValue(const TVariableType &,
                IndexType SolutionStepIndex)
{
    // Accessing to database and
    // returning value ...
}
template <class TVariableType>
typename TVariableType ::Type const &
Node ::GetValue(const TVariableType &,
                IndexType SolutionStepIndex) const
{
    // Accessing to database and
    // returning value ...
}

~~~

同时，()操作符可以被重写，以提供一个简单的接口。(注意：[]运算符不能接受一个以上的参数，不能在这里使用）。)

~~~C++
template <class TVariableType>
typename TVariableType ::Type &
Node ::operator()(const TVariableType &,
                  IndexType SolutionStepIndex)
{
    return GetValue(rThisVariable,
                    SolutionStepIndex);
}
~~~


在实践中，这个接口可能会更复杂，因为历史记录不一定要存储所有的变量，有单独的容器来存储历史变量和非历史变量是很有用的。例如，在Kratos中，有两组访问方法，以给出只存储需要的历史记录而不存储所有的历史记录的可能性。

\subsubsection{Elemental Interface} 

在`Elements`中，访问方法可以像`Nodes`那样实现。最好是尽可能地保持接口不变，以避免由于接口不一致而导致的额外工作。所以我们把访问方法和以前一样，拿一些其他的方法来做例子。

计算局部矩阵和向量的方法也可以用VBI来实现。但是在这里使用VBI的好处是什么呢？计算每个`Element`的局部贡献需要几个额外的参数，如时间步长、当前时间、delta时间等等。这些参数在不同的公式中可能是完全不同的，而且作为单独的参数传递所有这些参数在某种程度上是不可能的。一种方法是创建一个辅助类来封装这些参数，并通过这个辅助类传递所有的参数。同样，VBI在这里也可以被用来在这个工作层面上做一个统一的接口。在Kratos中有一个名为`ProcessInfo`的辅助类，它被传递给计算本地系统的方法，如下所示。

~~~C++
virtual void SomeElement ::CalculateLocalSystem(
    MatrixType &rLeftHandSideMatrix,
    VectorType &rRightHandSideVector,
    ProcessInfo &rCurrentProcessInfo)
{
    // Getting process information
    double time = rCurrentProcessInfo[TIME];
    // Calculating local matrix and
    // vector ...
}

~~~




\subsubsection{Input-Output} 

编写一个具有足够灵活性的通用输入输出是一项复杂的任务。进行扩展会给许多代码带来问题，而IO没有足够的灵活性来处理新变量。

在现代应用中，一个 `parser` 用来读取输入文件并理解输入语法 [^54][^77] 。通常情况下，这个输入文件`parser`是代码的一个复杂部分，为任何由扩展添加的新变量修改它是很昂贵的，而且不可能没有bug。一个好的解决方案是使`parser`与`list`的变量一起工作，并在每次需要读取新变量时改变`list`。这样做在输入`parser`中添加新的扩展可以更容易，而`parser`本身不会被改变。输入文件`parser`太大，不能在这里作为例子添加，感兴趣的读者可以在Kratos`DatafileI0`类中找到它的工作版本。

写输出文件就像读输入一样，可以用变量来概括。这样，任何对库的新扩展都可以使用现有的输出程序来写出它的结果。下面是一个使用变量为GiD [^84][^83] 编写通用输出程序的例子。


~~~C++
void GidIO ::WriteNodalResults(Variable<double> const &rVariable,
                               NodesContainerType &rNodes,
                               double SolutionTag,
                               std::size_t SolutionStepNumber)
{
    GiD_BeginResult((char *)(rVariable.Name().c_str()),
                    "Kratos",
                    SolutionTag,
                    GiD_Scalar,
                    GiD_OnNodes, NULL, NULL, 0, NULL);
    for (NodesContainerType ::iterator i_node = rNodes.begin();
         i_node != rNodes.end(); ++i_node)
        GiD_WriteScalar(i_node->Id(),
                        i_node->GetSolutionStepValue(rVariable,
                                                     SolutionStepNumber));
    GiD_EndResult();
}

void GidIO ::WriteNodalResults(Variable<Vector> const &rVariable,
                               NodesContainerType &rNodes,
                               double SolutionTag,
                               std::size_t SolutionStepNumber)
{
    GiD_BeginResult((char *)(rVariable.Name().c_str()),
                    "Kratos",
                    SolutionTag,
                    GiD_Vector,
                    GiD_OnNodes, NULL, NULL, 0, NULL);
    for (NodesContainerType ::iterator i_node = rNodes.begin();
         i_node != rNodes.end(); ++i_node)
    {
        array_1d<double, 3> &temp =
            i_node->GetSolutionStepValue(rVariable, SolutionStepNumber);
        GiD_WriteVector(i_node->Id(), temp[0], temp[1], temp[2]);
    }
    GiD_EndResult();
}


~~~




在上面的例子中，GiD接口对标量变量和矢量变量有不同的规则。知道变量的类型有助于为每种类型的变量实现定制的`WriteNodalResults`版本。这是该接口的一个重要特征，它可以处理某些类型的特殊情况，并以统一的语法为用户处理。

~~~C++
// Writing temperature of all the nodes
gid_io.WriteNodalResults(TEMPERATURE, mesh.Nodes(), time, 0);
// Writing velocity of all the nodes
gid_io.WriteNodalResults(VELOCITY, mesh.Nodes(), time, 0);

~~~


如前所述，每个变量都有其名称，可以通过Name方法访问。这为我们提供了必要的信息，以便在输出中打印该变量。这样一来，就不需要把变量的名字作为参数本身来传递。虽然这并不困难，但它保持了接口的简单性。

\subsubsection{Error Estimator} 

编写一个错误估计器是使用VBI制作一个通用和可重用代码的另一个例子。这里有一个以通用方式实现的简单的恢复性错误估计器[^104]的例子。

~~~C++
virtual void EstimateError(const VariableType &ThisVariable,
                           ModelPart &rModelPart)
{
    mpRecovery->Recover(ThisVariable, rModelPart);
    double e_sum = double();
    typedef ModelPart ::ElementIterator iterator_type;
    for (iterator_type element_iterator = rModelPart.ElementsBegin();
         element_iterator != rModelPart.ElementsEnd();
         ++element_iterator)
    {
        double error = CalculateError(ThisVariable, *element_iterator);
        element_iterator->GetValue(ERROR) = error;
        e_sum += error;
    }
    SetGlobalError(e_sum);
}

double CalculateError(const VariableType &ThisVariable,
                      Element &rThisElement)
{
    Element ::NodesArrayType &element_nodes = rThisElement.Nodes();
    if (element_nodes.empty())
        return double();
    double result = 0.00;
    typedef Element ::NodesArrayType ::iterator iterator_type;
    for (iterator_type node_iterator = element_nodes.begin();
         node_iterator != element_nodes.end(); ++node_iterator)
    {
        TDataType error = CalculateError(ThisVariable,
                                         rThisElement,
                                         *node_iterator);
        result += sqrt(error * error);
    }
    result *= rThisElement.GetGeometry()->Area();
    return result / element_nodes.size();
}
TDataType CalculateError(const VariableType &ThisVariable,
                         Element &rThisElement,
                         Node &rThisNode)
{
    TDataType result = rThisNode[ThisVariable];
    result -= rThisElement.Calculate(ThisVariable, rThisNode);
    return result;
}

~~~





在这种方式下，误差估计器不依赖于领域，并且可以以同样的方式工作于热流或压力梯度。

\subsection{Problems and Difficulties} 

像往常一样，没有什么是完美的，VBI也不排除这种情况。在使用这个界面时，仍有一些困难和问题。虽然它们在某些情况下不是很重要，但在另一些情况下，它们需要更多的关注。

一个问题是要存储一个可以被替换为组件的变量。在什么地方会出现这种情况呢？任何以相同方式处理变量或其组件的过程，并希望存储变量或组件，都会造成一些困难。一个典型的例子是在一些双数上的过程，如计算常数，并应用于一个`double`的变量，如温度，或一个`vector`的分量，velocity_x，它也想保留一个给定的变量，以便之后对其工作。在许多情况下，这可以很容易地解决，只需将数据类型模板参数替换为变量类型参数。这种模式可以与`GetValue`和`SetValue`方法的统一模板版本相同。

但仍有一些情况是上述方法不能轻易解决的。储存自由度的变量是一项复杂的任务。假设每个自由度都想存储它的变量，然后搜索数据库以找到它的值，例如更新结果。

~~~C++
// Updating
typedef EquationSystemType ::DofsArrayType ::iterator iterator_type;
for (iterator_type i_自由度 = equation_system.DofsBegin();
     i_自由度 != equation_system.DofsEnd(); ++i_自由度)
{
    if (i_自由度->IsFree())
        i_自由度->GetSolutionStepValue() =
            equation_system.GetResults()[i_自由度->EquationId()];
}
~~~



在这种情况下，给`Dofs`提供不同的模板参数使我们无法在正常的数组中使用它们，而且由于这些方法在有限元代码中的内循环使用，使用虚拟函数是不允许的。那么该怎么做呢？在Kratos中，一个索引机制被用来决定变量是否是一个组件，一些`traits`[^99]被用来调度和选择适当的程序。这个解决方案不是那么干净，而且应用起来还不是那么封装。改进这些任务的进一步工作仍有待于在未来完成。

~~~C++
TDataType &GetReference(VariableData const &ThisVariable,
                        FixDataValueContainer &rData,
                        int ThisId)
{
    switch (ThisId)
    {
        KRATOS_DOF_TRAITS
    }
    KRATOS_ERROR(std::invalid_argument, "Not supported type for Dof", "");
}

~~~







\section{Data Structure} 

数据结构是有限元程序的主要部分之一。有限元代码功能的许多限制来自于其数据结构设计。Kratos '数据结构必须提供高度的灵活性，这是处理多学科问题的必要条件。

在本章中，首先对数据结构编程中的不同概念进行了简要描述。然后，解释了一些经典的容器，讨论了它们的优点和缺点。它继续介绍了适合多学科有限元编程的新容器的设计和实现。之后，解释了有限元编程中一些常见的数据组织，最后介绍了Kratos中数据结构的组织。

\subsection{Concepts} 

在这一节中，对数据结构编程中的常见概念进行了简要描述。关于这里描述的概念以及该领域的其他概念的更多信息可以在 [^55][^14][^46] 中找到。

\subsubsection{Container} 

 `Container` 是一个存储其他对象的对象，并给出一些方法来访问这些对象，添加新的对象，或从其中删除一些对象。存储在容器中的每个对象都被称为它的一个元素。 `Container`必须提供一些方法来创建和修改它，以及一些访问它的元素的方法。下面是一些常见的方法的`list`。

\textbf{Access} 在给定的位置提供元素。根据容器的不同，可以通过位置或一些参考键来实现访问。通常，[]操作符为容器提供这个接口。

\textbf{Insert} 在给定的位置之后插入一个给定的元素。向容器中添加元素会增加其大小。

\textbf{Append} 将给定的元素加到最后。将元素添加到一个容器中，增加其大小。

\textbf{Erase} 移除指定位置的元素。这个操作通过删除元素的数量来减少容器的大小。查找 在容器中搜索一个具有给定规格的元素。

\textbf{Size} 获取容器的大小。

\textbf{Resize} 改变容器的大小。

\textbf{Swap} 将容器的内容与给定的内容交换。

由于结构的原因，一些容器可能不会提供所有这些方法，而另一些则可能为其特定用途提供一些更多的方法。另外，这些操作的性能在很大程度上取决于容器的内部结构。所以在使用一个容器之前，研究它在算法所需操作方面的性能是非常重要的。

\subsubsection{Iterator} 

通常情况下，一个被称为迭代器的指针被用来访问容器中的元素。下面是一个使用指针作为$\mathrm{C}$数组迭代器来打印其内容的例子。

~~~C++
double data[^10];
// putting values in data
// ...
// now printing data using an iterator
double *data_end = data + 10;
for (double *i = data; i != data_end; i++)
    std::cout << *i << std::endl;

~~~



`Iterator`模式定义了一个通用的指针，用于顺序访问一个容器的元素，而不暴露其内部结构。迭代器可以被用来以一般的方式逐个遍历容器中的元素，而不知道它们在内存中的真实存储方式。将前面例子中的$\mathrm{C}$数组改为容器，给出了一个以一般形式使用迭代器的例子。

~~~C++
ContainerType data;
// putting values in data
// ...
// now printing data using an iterator
typedef ContainerType ::iterator iterator_type;
for (iterator_type i = data.begin(); i != data.end(); i++)
    std::cout << *i << std::endl;

~~~



在这个例子中，任何提供一个迭代器和两个方法来表示其开始和结束位置的容器都可以用来打印其内容。

`Iterator`可以被设计为以不同的方式遍历容器。例如，一个矩阵可以有不同的迭代器。


\textbf{iterator} 遍历从$a_{11}$到$a_{m n}$ 的所有成员。

\textbf{row_iterator} 遍历矩阵的行。

\textbf{column_iterator} 遍历矩阵的列。

\textbf{nonzero_iterator} 遍历所有非零元素。


这使得迭代器成为以通用方式访问容器的一个非常强大的工具。对于使用迭代器的算法来说，不需要知道容器本身，这使得它们更加通用。

根据容器的结构，有些遍历是不可能的或者是没有意义的。所以不同的容器由于其限制而使用不同类别的迭代器。下面是迭代器的主要类别。

\textbf{Forward Iterator} 一个简单的迭代器，只允许移动到下一个元素。这种迭代器不能用来往回走。例如，查看迭代器位置的前一个元素。前进迭代器每一步只能向前移动一个元素，不能用来跳过某个偏移。

\textbf{Bidirectional Iterator} 与前向迭代器不同，这个迭代器可以用来向后遍历容器。但是仍然可以每次向前和向后移动一步。

\textbf{Random Access Iterator} 这个迭代器可以自由地向前和向后移动，也可以跳到由偏移量给出的任何其他位置。


\subsubsection{List} 

列表是一个元素的序列，可以根据它们在 `list` 上的位置进行线性排序。一个`list`通常由一个逗号分隔的元素序列表示。

\begin{equation}
a_{1}, a_{2}, \ldots, a_{i-1}, a_{i}, a_{i+1}, \ldots, a_{n}
\end{equation}

在一个`list`中，重要的是元素的相对位置。所有的元素都在同一层次，它们之间唯一的关系是下一个元素或上一个元素。例如，元素$a_{i-1}$在$a_{i}$之前，$a_{i+1}$是下一个。

有几个结构代表`list`。它们中的每一个都以不同的方式存储其元素，并提供不同的属性，我们将在后面看到。

\subsubsection{Tree} 

树是一个分层的元素集合，被称为 `nodes` 。与 `list` 不同的是，树中的元素不在同一层次，有些元素被认为是其他元素的父元素。在每棵树上都有一个叫做`root`的节点，它是整个树的父节点。

\subsubsection{Homogeneous Container} 

一个只能存储一种类型元素的容器是一个 `homogeneous container` 。例如，一个C语言的双数数组是同质的，而只能存储`double`变量。

~~~C++
double homogeneous_array_of_doubles [3];
~~~


即使是一个整数也必须先转换为`double`，然后才能存储在这个数组中。C++标准容器是同质的，只能存储一种类型的元素。

\subsubsection{Heterogeneous Container} 

如果一个容器能够存储不同类型的元素，就被认为是`heterogeneous`。图[7.1]显示了内存中的一个异构容器。

通常情况下，异质容器的实现可以接受任何类型的数据，但在某些情况下，实现一个同样是异质的但只能存储某些类型元素的容器更为方便。在这项工作中，这种类型的容器将被称为 `quasi heterogeneous containers` 。

\subsection{Classical Data Containers} 

本节将简要介绍一些通常在有限元程序中使用的经典数据容器，并讨论它们的优缺点。关于数据容器及其实现的更多信息可以在 [^55][^14][^46][^102] 中找到。

\subsubsection{Static Array} 

静态数组是`list`的一种实现，它将其元素按顺序放在内存中，中间没有任何间隙。图[7.2]显示了内存中的一个数组。



静态数组在构建时可以存储一定数量的元素。例如，下面这个双数数组最多可以存放10个双数。

~~~C++
double results [10];
~~~

这个限制使得静态数组无法用于可变大小和不断增长的容器。

\textbf{Interface and Operations} 

由于数组的`restricted`功能，它的接口非常简单。

\textbf{Access} 通常，操作符[]被用来访问数组的元素。访问的速度非常快，而且与数组的位置和大小有关的时间是不变的。这意味着访问数组中任何一个元素的时间都不取决于它的位置，也不取决于容器中的元素数量。访问一个元素是通过偏移基指针的位置索引完成的，如图所示 [7.3] 。

\textbf{Size} 为了提供大小接口，数组必须存储其大小，这意味着开销，特别是在小数组的情况下。对于某些实现来说，这种开销是可以消除的，我们将在后面看到。





\textbf{Swap} 一般来说，交换可以逐个元素进行，这导致交换时间与容器的大小成线性关系。在某些情况下，这可以通过指针下来，这使得它成为一个恒定的时间操作，因为两个数组只是改变它们的指针序列，而不是一个一个地改变所有元素。

如前所述，数组的大小不能被改变，所以静态数组没有调整大小的接口。出于同样的原因，也没有插入、追加和擦除接口，而这些接口会改变容器的大小。

\textbf{Advantages} 

- 对系统缓存的使用非常好。阵列的顺序性使得它的缓存效率非常高，这可以使其性能得到很大的提高。

- 遍历一个数组是非常快的。在任何时候，下一个和上一个元素都是已知的，使用缓存使得迭代非常快。

- 每个元素都可以通过其序列号快速访问，因为知道这个序列号就可以知道它的位置，而不需要对数组进行迭代。

- 对静态数组元素的循环可以通过解开循环来优化小数组，而元素的数量可以在编译时就知道。

- 每个元素没有内存开销。在一些实现中，甚至每个容器没有内存开销。这使得它在需要大量的小容器时成为一个非常好的选择。

\textbf{Disadvantages} 

- 没有办法插入一个新的元素或擦除一个退出的元素。这使得它不适合于可变大小的序列。

- 数组的大小在创建时必须是已知的，这使得它不能用于增长的元素序列。

一般来说，静态适合于小型和刚性的容器。例如，一个3维的点可以被实现为一个3个元素的静态数组。

\textbf{Implementation} 

 $\mathrm{C}$（以及随后的$\mathrm{C}++)$提供了一个静态数组本身。这个数组是一个数组的最小实现，它提供了一个指针来迭代它，以及一个[]操作符来访问它的数据。 $\mathrm{C}$数组不提供大小信息，这意味着用户需要向他们传递$\mathrm{C}$数组的任何算法提供这一额外信息。

简单的$\mathrm{C}$数组可以使用$\mathrm{C}++$模板来改进，如下所示。

~~~C++
template <class TDataType, std::size_t TSize>
class array
{
    TDataType mElements[TSize];

public:
    // type definitions
    typedef T value_type;
    typedef T *iterator;
    typedef const T *const_iterator;
    typedef T &reference;
    typedef const T &const_reference;
    typedef std::size_t size_type;
    typedef std::ptrdiff_t difference_type;
    // iterator support
    iterator begin() { return mElements; }
    const_iterator begin() const { return mElements; }
    iterator end() { return mElements + TSize; }
    const_iterator end() const { return mElements + TSize; }

    // access operator []
    reference operator[](size_type i)
    {
        return mElements[i];
    }
    reference operator[](size_type i) const
    {
        return mElements[i];
    }
    static size_type size() { return TSize; }
    // element by element swap (linear complexity)
    void swap(array<T, TSize> &y)
    {
        std::swap_ranges(begin(), end(), y.begin());
    }
    // c array representation
    T *c_array() { return mElements; }
};

~~~

这个实现提供了数组的大小，没有任何存储开销。同时，它与STL兼容，可以传递给STL算法，这给了它另一个附加价值。在这个项目中，使用了数组的boost[^3]实现，它被宣布为$\mathrm{C}++$标准草案的一部分。

\subsubsection{Dynamic Array} 

有很多情况下，数组的确切大小是不知道的，但可以给出一个最大的大小。这个最大的尺寸可以作为数组的容量。所以数组可以用一个给定的容量来构建，并用于存储任何数量的小于容量的元素。这里需要一个额外的指针，指向存储元素的尾部，以指示数组的实际大小。图[7.4]显示了这个数组的实现。

\textbf{Interface and Operations} 

\textbf{Access} 通常，操作符[] 被用来访问数组的元素。像静态数组的访问是非常快的，而且时间上是恒定的。这意味着访问数组中任何元素的时间都不依赖于它的位置，也不依赖于容器中元素的数量。访问一个元素是通过从零开始通过位置索引偏移基指针来完成的，如图所示 [7.5] 。





\textbf{Insert} 在一个位置上插入一个元素，可以通过将该位置之后的所有元素移到下一个位置来完成，以便为新元素腾出空间，然后将该元素放在准备好的位置上。这个操作对于要移位的元素的数量来说是线性的。图[7.6]显示了这个程序。




很明显，这个过程在数组的容量超过其大小之前是有效的。否则就需要一个调整大小的程序。

\textbf{Append} 与插入不同，将一个元素附加到数组的末端不需要任何移位，并且需要恒定的时间，与数组的大小无关。图[7.7]显示了这个过程。同样的，如果数组是满的，那么追加就会随着容量的变化而调整大小，这使得它的效率降低。




\textbf{Erase} 为了擦除阵列中间的一个元素，需要一个移动过程来重建阵列的连续性。擦除包括删除该元素，然后将其余的元素后移一步以填补空缺。图[7.8]显示了这个过程。



\textbf{Size} 阵列的大小可以通过阵列的开始和结束的距离来计算，也可以根据实现方式来存储。一般来说，这是一个快速的过程，在时间上与容器的大小有关，是恒定的。

\textbf{Resize} 在不改变容量的情况下调整大小是简单有效的，但是改变容量就比较复杂和低效。改变一个数组的容量意味着重新分配内存。如果有空闲的内存，那么数组的重新分配可以不用复制，但是如果内存被分配，那么整个数组必须被复制到其他有足够空间的地方，如图[7.9]所示。这导致数组元素的指针无效，而它们仍然指向内存中数组的前一个位置。





 \textbf{swap} 一般来说，交换可以逐个元素地进行，这导致交换时间与容器的大小成线性关系。但是在某些情况下，这可以通过指针下来，这使得它成为一个恒定的时间操作，因为两个数组只是改变它们的指针序列，而不是一个一个地改变所有元素。

\textbf{Array Advantages} 

- 非常好地利用了系统缓存。数组的顺序性使得它的缓存效率非常高，这可以使其性能得到很大的提高。

- 遍历一个数组是非常快的。在任何时候，下一个和上一个元素都是已知的，使用缓存使得迭代非常快。

- 每个元素都可以通过其序列号快速访问，因为知道这个序列号就可以知道它的位置，而不需要对数组进行迭代。

- 每个元素没有内存开销。只有一个小的开销，就是为每个容器存储指针。

\textbf{Array Disadvantages} 

- 由于移位过程，在数组中间插入元素的速度相对较慢。

- 增加`vector`的容量需要重新分配内存，这使得它很慢。保留额外的内存可以解决这个问题，但要付出内存开销的代价。

- 移位和重新分配使指向数组元素的指针无效，这使得元素的引用成为一项困难的任务。尽管使用`vector`的序列号而不是直接引用可以解决这个问题。

- 阵列不会自动减少容量。例如，从数组中移除元素不会减少数组使用的内存。

\textbf{Interface and Operations} 

\textbf{Access} 通常情况下，运算符[]被用来访问数组中的元素。访问的速度非常快，而且相对于数组的位置和大小，访问的时间是恒定的。这意味着访问数组中任何一个元素的时间都不取决于它的位置，也不取决于容器中元素的数量。访问一个元素是通过从零开始通过位置索引偏移基指针来完成的，如图所示 [7.10] 。





\textbf{Insert} 在一个位置上插入一个元素，可以通过将该位置之后的所有元素移到下一个位置来完成，以便为新元素腾出空间，然后将元素放到准备好的位置。这个操作与要移位的元素的数量成线性关系。图[7.11]显示了这个程序。

很明显，这个过程在数组的容量大于尺寸之前是有效的。否则就需要一个调整大小的过程。





\textbf{Append} 与插入不同，将一个元素附加到数组的末端不需要任何移位，并且需要恒定的时间，与数组的大小无关。图[7.12]显示了这个过程。同样的，如果数组已经满了，追加会导致容量的变化，使其效率降低。




\textbf{Erase} 为了擦除数组中间的一个元素，需要一个移位过程来重建数组的连续性。擦除包括删除该元素，然后将其余的元素后移一步以填补空缺。图[7.13]显示了这个过程。




\textbf{Size}数组的大小可以通过数组的开始和结束的距离来计算，也可以根据实现情况来存储。一般来说，它是一个快速的过程，与容器的大小有关的时间不变。

\textbf{Resize} 在不改变容量的情况下调整大小是简单有效的，但是改变容量就比较复杂和低效。改变一个数组的容量意味着重新分配内存。如果有空闲的内存，那么数组的重新分配可以不用复制，但是如果内存被分配了，那么整个数组必须被复制到其他有足够空间的地方，如图所示 [7.14] 。这导致数组元素的指针无效，而它们仍然指向内存中数组的前一个位置。





\textbf{Swap} 一般来说，交换可以逐个元素地进行，这导致交换时间与容器的大小成线性关系。但是在某些情况下，这可以通过指针下来，这使得它成为一个恒定的时间操作，因为两个数组只是改变它们的指针序列，而不是一个一个地改变所有元素。

\textbf{Implementation} 

如前所述，新的元素可以很容易地追加到`list`的尾部，但在`list`的中间插入一个元素需要所有的后续元素转移，为新的元素腾出空间。同样，从尾部移除元素也很容易，而从中间移除则比较复杂，因为后面的元素要进行移位，以弥补缺口。移位过程导致在`list`的中间插入和移除元素的速度变慢。它还会使所有指向被移位元素的指针失效。这导致了通过指针访问数组元素的问题。例如，以下从给定数据中移除负值的代码将无法工作，因为移除每个元素都会改变最后一个元素的位置，所以序列的结束。

~~~C++
double *begin = data.begin();
double *end = data.end(); // Will be invalidated by remove !!
for (double *i = begin; i != end; i++)
    if (*i < 0.00)
        data.remove(i); // Invalidates the end pointer !!

~~~


在一个已经满的数组中添加任何元素都需要增加容量。这个操作很慢，而且每增加一个元素都会超过容量，导致性能不佳。保留更多以后使用的元素是解决这个问题的一个常见方法。每次需要增加容量时，就会分配额外的内存以避免为下一个元素重新分配。尽管这种方法有效地解决了性能问题，但却带来了内存开销。较大的缓冲区提供了更好的性能，同时也有较大的内存开销。所有这些都可以通过创建一个具有正确容量的数组来避免。

`vector` 类提供了C++标准库中的数组实现。

~~~C++
vector<class T, class Alloc = Allocator<T>>
~~~

第一个模板参数 $\mathrm{T}$ 是要存储的元素的类型，第二个是管理 `vector` 所用内存的分配器。 `vector`自动调整其容量，并且还保留了一个额外的内存以提高超尺寸插入的性能。缓冲区的大小因实现方式不同而不同，但通常是$50 \%$或$100 \%$的`vector`大小。这可能对某些情况造成不可接受的开销，必须通过为 `vector` 分配正确的容量来避免。



\subsubsection{ Singly Linked List } 

单链`list`是一组的元素通过指针相互连接的链子。主要的想法是给每个元素一个指向下一个元素的指针。因此，有了第一个元素，`list`就可以通过从每个元素到下一个已知的位置进行遍历。图[7.15]显示了内存中的单链`list`。





\textbf{Interface and Operations} 

访问 在单链`list`中访问一个知道其序列号的元素并不像阵列那样容易。为了访问一个元素，必须从`list`的头部开始，通过`list`到达指定的位置。例如，要找到`list`中的第五个元素，首先必须用头部来找到第一个元素，然后第一个元素有一个指向第二个元素的相关指针。到第二个元素，我们可以得到第三个元素的指针，然后从第三个到第四个，最后到第五个位置。这种搜索性质使得单链`list`中的索引是一个非常缓慢的过程。有些实现甚至不提供按位置的访问。

插入 在给定的元素之后插入一个新的元素是非常快的，不需要移位。插入的过程包括使新元素指向给定元素已经指向的元素，并使给定元素指向新元素作为其下一个元素。图[7.16]显示了这个过程。





\textbf{Append} 对于链接的`list`附加和插入具有相同的性质，可以用同样的方式完成。唯一的区别是，最后没有指向的元素，所以追加是为了使最后一个元素指向新的元素，如图[7.17] 所示。

\textbf{Erase} 像插入一个新元素一样，擦除一个现有的元素也非常简单和有效。要在一个给定的元素之后擦除一个元素，只需要让给定的元素指向被删除的元素之后，然后从内存中删除它，如图7.18所示。擦除一个元素的时间复杂性与它的位置和数组的大小有关，是恒定的。

\textbf{Size} 获取一个链接`list`的大小不像数组那样容易。事实上，要想知道有多少元素被连接在一起，必须对`list`进行完整的遍历。因此，接受开销并存储`list`的大小以提高其性能是很方便的。








\textbf{Resize} 虽然`list`没有最大尺寸或任何`restricted`尺寸，但这个过程包括添加新元素或删除一些其他元素，以达到`list`的指定尺寸。

\textbf{Swap} 交换两个列表非常简单，只包括交换两个列表的头部指针。图[7.19]显示了这个过程。




\textbf{Singly Linked List Advantages} 

- 在一个给定的元素之后快速插入一个新元素。

- 高效地删除一个元素。

- 一个链接的`list`可以通过添加新的元素而平稳地增长，而且不需要调整它的大小，也不需要像动态数组那样创建一个缓冲区。

- 从列表中删除元素会自动减少其在内存中的大小，这就避免了相应的开销或手动控制其大小。

\textbf{Singly Linked List Disadvantages} 

- 单链`list`不能反向遍历，因为每个元素只知道它的下一个邻居，而不知道上一个。所以这个容器不能用于需要向前和向后遍历容器的算法。

- 按位置访问是一个非常慢的任务，必须避免。这种访问可以通过保留指向`list`的必要元素的指针来减少，以供将来使用。

- 由于列表中的元素可以在内存中任意分布，因此列表的缓存效率较低。这使得它们在实践中作为快速迭代容器的吸引力降低。

因此，单链式`list`适合于高度可变的数据结构，受制于有大量元素插入和删除语句的程序。

\textbf{Implementation} 

不幸的是，C++标准库并不支持这种容器。有一些标准库的扩展提供了`slist`[^28][^8]作为标准`list`容器的单链接`list`变体。

在Kratos中，在`ProcessInfo`类中实现了单链接`list`的修改版本，以便在有限元程序中跟踪其历史。

\subsubsection{ Doubly Linked List } 

如上所述，单链接的`list`有一个缺点，即不能以反向顺序遍历。双链式`list`通过为其元素添加另一个指针来解决这个问题，该指针将它们与之前的元素联系起来。通过这种方式，双链式`list`可以以两种方式进行遍历，但会造成每个元素一个指针的开销。图[7.20]显示了内存中的一个双链`list`。





\textbf{Interface and Operations} 

\textbf{Access} 在双链路`list`中访问一个知道其序列号的元素，需要与单链路`list`的程序相同。同样，要访问一个元素，必须从`list`的头部开始，通过`list`到达指定的位置。例如，要找到`list`的第五个元素，首先必须用头部来找到第一个元素，然后第一个元素有一个指向第二个元素的相关指针。到第二个元素，我们可以得到第三个元素的指针，然后从第三个元素到第四个元素，最后到第五个位置。这种搜索性质使得在双链`list`中建立索引成为一个非常缓慢的过程。有些实现甚至不提供按位置的访问。插入 在给定的元素之后插入一个新的元素是非常快的，不需要移位。它包括将给定位置的元素链接到新的元素，以及将新元素链接到给定位置的下一个元素。图[7.21]显示了这个过程。





附加 对于一个链接的`list`附加和插入具有相同的性质，可以用相同的方式完成。唯一的区别是，最后没有指向的元素，所以追加只是让最后的元素指向新的元素，如图所示 [7.22] 。





\textbf{Erase} 像插入一个新元素一样，擦除一个现有的元素是非常简单和有效的。要在一个给定的元素之后擦除一个元素，只需要让给定的元素指向移除的元素之后，然后从内存中删除它，如图所示[7.23] 。擦除一个元素的时间复杂性与它的位置和数组的大小有关，是恒定的。





\textbf{Size} 获取一个链接`list`的大小不像数组那样容易。事实上，要想知道有多少元素被连接在一起，需要对`list`进行完整的遍历。因此，接受开销并存储`list`的大小以提高其性能是很方便的。调整大小 当`list`没有最大的大小或任何`restricted`的大小时，这个过程包括增加新的元素或删除一些其他的元素，以达到`list`的既定大小。

\textbf{Swap} 交换两个列表是非常简单的，只包括交换两个列表的头部指针。图[7.24]显示了这个过程。





\textbf{Doubly Linked List Advantages} 

- 在一个给定的元素之后或之前快速插入一个新元素。

- 高效地删除一个元素。

- 一个链接的`list`可以通过添加新的元素而平稳地增长，而且不需要像动态数组那样调整它的大小或创建缓冲区。

- 从列表中删除元素会自动减少其在内存中的大小，从而避免相应的开销或手动控制其大小。

- 双链式 `list` 可以以两种方式遍历，这使得它比单链式 `list` 可用于更广泛的算法。

\textbf{Doubly Linked List Disadvantages} 

- 按位置访问是一个非常慢的任务，必须避免。这种访问可以通过保留必要元素的指针来减少 `list` ，以便将来使用。

- 由于列表中的元素可以在内存中任意分布，因此列表的缓存效率较低。这使得它在实践中作为一个快速迭代容器的吸引力降低。

- 当元素很小的时候，每个元素的两个指针的开销是很明显的。例如，对于一个`list`的双倍数来说，这些指针在32位的编译中会重复占用内存，在64位的编译中甚至更糟。

这种容器适合于存储中间有大量插入和擦除的数据。

\textbf{Implementation} 

C++标准库提供了一个 `list` 类来表示双链接 `list` 。这个类通过模板参数化，接受不同的类型。

~~~C++
list <class T, class Alloc = Allocator <T> >
~~~

\subsubsection{Binary Tree} 

二叉树是一棵树，其中每个节点要么没有孩子，要么只有一个`left child`，要么只有一个`right child`，要么既有左孩子又有右孩子。换句话说，二叉树中的每个节点都有两个下行分支，即左和右，它们可以是空的，也可以不是。图[7.25]显示了一个二叉树。





值得一提的是，左和右孩子的顺序是树规范的一部分，交换一个节点的左孩子和右孩子会产生另一棵与原来不同的二叉树。例如，图[7.26]中的树由于子节点位置的改变而不相等。




二叉树可用于对返回真或假的二进制比较运算符进行数据排序。例如，小于运算符$<$可以用来对二进制树中的`set`个数字进行排序，如图7.27所示。在这棵树中，每个节点都大于其左边子树中的所有数值，小于或等于其右边子树中的所有数值。这种排序可以在以后用于在树中搜索一个值。





\textbf{Interface and Operations} 

\textbf{Access} 在二叉树中，通常不按位置提供访问，访问通常是通过寻找树中的一个键来完成的。

\textbf{Insert} 在树中插入一个新元素包括找到它的位置，然后把它添加到那里。在一些实现中，一个给定的位置也被接受，但作为一个提示，可以更快地找到正确的位置。这种搜索对于保证树的顺序是必要的。寻找正确的插入位置的程序从根部开始，将新元素的键与根部进行比较。如果结果是真的，就去找左边的子节点，如果是假的，就去找右边的子节点并重复比较。这个过程重复进行，直到有一个空的子节点成立，这就是要插入元素的位置。有了这个位置，插入只是将父代指向这个新的元素，就像在链接列表中插入一样。图[7.28] 显示了在图[7.27] 的有序树中插入值7的过程。





\textbf{Find} 要在树中找到一个值，可以从根部开始，并与该值进行比较。如果比较结果为真，意味着该值一定在左子树中，如果为假，意味着该值一定在右子树中。有了相应的子树，就可以重复这个过程，看看哪个子树应该有这个值。这个过程要么到达我们想要的变量，要么到达一个空分支，表示该值不存在。树的排序和分层性质使得寻找算法非常有效。这种搜索所需的比较次数保证为$O(\ln N)$，比在无序的正常数组中搜索所需的$O(N)$比较次数低得多，特别是对于大量的实体$N$ 。图[7.29] 显示了在图[7.28] 的有序树中寻找值9的这个过程。





\textbf{Erase} 擦除二叉树的一个元素需要替换一些元素，以保持树的排序。如前所述，排序确保一个节点的值与它的左子树中的所有值比较是真的，与它的右子树中的所有值比较是假的。在使用小于运算符$<$进行比较的情况下，可以说左子树的所有值都小于节点值，右子树的所有值都大于它。可以看出，为了保持这种排序，我们必须取最小值，或右子树的最左边的值，并把它放在被删除的节点的位置。例如，从图[7.29]的二叉树中删除6，首先要找到右子树的最左节点，即7，并将其放在6的位置上。然后用其右边的子节点填补被移动的最小节点的空位，在这种情况下是空的。由此产生的二叉树可以在图[7.30]中看到  。





\textbf{Iterating} 与数组和链表不同，对有序二叉树的迭代并不那么简单，它包括对树结构的升序和降序，以保持对元素序列的跟踪。迭代器在每个子树中找到最左边的元素，并试图通过在树层中上下移动来从左到右地进行迭代，其思路是以起始节点为起点，进入其右边的子树。图[7.31] 显示了从元素1到元素12的迭代器路径。

\textbf{Size} 像链接列表一样，获取二叉树的大小并不容易，需要在树上进行遍历。因此，接受这种开销并将树的大小存储在一个成员变量中以提高其性能是很方便的。





\textbf{Swap} 交换两个二叉树非常简单，只需要交换两个树的根指针。

\textbf{Binary Tree Advantages} 

- 始终保持元素的顺序是一个重要的特征，特别是在处理元素的变量集合时。

- 快速查找程序。二叉树的分层结构和顺序性质使我们能够对大量的元素进行非常有效的二进制搜索。

- 相对快速地插入一个新元素，同时保持元素的顺序。

- 在不改变元素的顺序的情况下，再次删除元素的效率相对较高。

\textbf{Binary Tree Disadvantages} 

- 一般情况下没有按位置访问。它们是专门用于按键而不是按位置访问的。

- 二进制列表没有缓存效率，因为它们的元素没有按顺序存储在内存中。

- 对二叉树进行迭代是一个复杂的过程，因此效率不高。

- 当元素很小的时候，每个元素的三个指针的开销是很明显的。

\textbf{Implementation} 

幸运的是，C++标准库提供了各种以不同方式表示二叉树的类。第一个是`set`，它是一个其模板参数类型的有序二叉树。它还需要一个可选的比较运算符，这使得它更加通用。 `set`使用这种比较来理解一个元素的顺序。





\subsubsection{Quadtree} 

四叉树是一种特殊类型的树，其中每个节点有零到四个子节点。图[7.32]显示了一个一般的四叉树。

这种结构对于组织二维空间的数据非常有用，因为它可以用来处理分区信息，而分区信息以分层形式定义了空间中的单元。图[7.33]显示了一个由四叉树划分的领域。由于这个原因，四叉树节点中的子节点的名称来自它们在二维`map`中的相对位置：西北、东北、西南和东南。

如上所述，二叉树可用于组织一维的数据。四叉树在二维空间中也有同样的作用。它可以与两个比较运算符一起使用，以尊重其坐标的方式对点进行排序。例如，使用两个小于运算符$<$可以得到一个顺序四叉树，其中每个节点的坐标$x$大于其西北和西南子树的所有坐标$x$，其$y$坐标大于西南和东南子树的所有坐标$y$，如图[7.34] 所示。

\textbf{Interface and Operations} 

\textbf{Access} 在四叉树中，访问一个元素是通过在树中找到一个`pair` 键，例如一个点的两个坐标来完成的。

\textbf{Insert} 在四叉树中插入一个新的元素包括找到它的位置，然后把它添加到那里。有时会接受一个给定的位置作为提示，以便更快地找到正确的位置。这个过程类似于二叉树，但使用两个比较来找到正确的分支。它从根开始，通过分支，直到一个分支导致一个空的子。有了这个位置，插入只是将父元素指向这个新元素。





查找程序从根开始，比较键，例如点的坐标，如果与之匹配，则找到结果，如果不匹配，则进入与比较结果相对应的分支。该过程对分支中的节点重复进行，直到找到实体或表示实体不存在的空叶。

四叉树的不同类型和每种类型的详细描述可以在[^91][^92]中找到。

\subsubsection{Octree} 

八叉树实现了与四叉树相同的概念，但却是在一个三维空间中。在八叉树中，每个节点有8个分支，这些分支可能会导致一个孩子，也可能不会。考虑到八叉树是四叉树的三维扩展，四叉树在二维空间中使用两个比较运算符进行的所有操作，现在可以由八叉树在三维空间中使用三个比较运算符进行。









\subsubsection{ k-d Tree } 

 $\mathrm{K}$ -d树是一种处理$\mathrm{k}$维空间的通用方法，只在每个节点使用`two-way`分支。图[7.35]显示了一个用于处理二维领域划分的k-d树。

\subsubsection{ Bins} 

在二维和三维空间中存储和查找对象的一个简单而有效的数据结构是Bins。它将域划分为一个有规律的$n_{x} \times n_{y} \times n_{z}$子域，并持有一个存储其元素的桶的阵列。图[7.36]显示了一个由Bins划分的二维域，图[7.37]显示了这个Bins的结构。

当实体或多或少均匀地分布在域上时，这种结构提供了快速的空间搜索。对分布良好的实体的良好性能和简单性使得Bins成为不同的有限元应用中流行的数据结构之一 [^59] 。

\subsubsection{Containers Performance Comparison} 

如前所述，每个容器根据其内部结构在操作和内存消耗方面提供不同的性能。这种差异被描述为每个容器的优势和劣势。然而，这种各自的性能也取决于容器的大小，并且可以随着大小的变化而发生根本性的变化。在这一节中，我们提供了容器之间在某些操作上的简要比较。这种比较给出了一些实验结果，表明了不同容器在实践中的行为。

值得一提的是，在这些测试中没有进行人工优化，只有编译器的自动优化是`set`到最高值。









\textbf{Memory usage of containers} 

在这个比较中，不同的容器被初始化为不同的大小，每个容器所使用的内存被测量。图[7.38]显示了这个比较的结果。

可以看出，`vector`使用的内存比其他容器少。原因是用于存储元素中的指针的内存。

另一个基准测试是为了了解不同容器的大小所使用的内存。为此，我们用不同的容器创建了一个大小为$n=100000$的数组，每个容器使用的内存如图[7.39]所示。

\textbf{Construction and Destruction} 

在这个测试中，不同容器的构建和销毁时间被比较。图[7.40]显示了不同容器的构建时间比较。

可以看出，`vector`的构建时间远远小于其他容器的构建时间。原因是它的内部结构比 `list` 或 `set` 更简单。图[7.41]显示了不同容器的销毁时间。

同样，`vector`的速度远远超过其他容器。这使得`vector`成为必须立即创建和删除容器的情况下的一个好选择。


另一个测试是比较容器作为堆栈内存中分配的局部变量的构建和销毁时间。虽然在程序中创建容器作为局部变量是很常见的，但重要的是要看到每次程序被调用时它们的构建和销毁时间的开销。图[7.42]显示了这种比较。



这次C语言的静态数组显示了更好的性能，甚至与`vector`相比。原因是为`vector`分配动态内存的开销，而$\mathrm{C}$静态数组则完全在堆栈中分配。因此，将程序中的小型本地容器实现为静态数组可以显著提高代码的性能。

\textbf{Iterating} 

许多有限元算法都习惯于在容器上进行迭代。由于这个原因，良好的迭代性能是选择容器的重要因素。这个基准包括对不同大小的样本容器的所有元素进行迭代。测量了$10^{9}$步迭代的时间，结果显示在图7.43。每一步都包括对迭代器内容的访问，以确保优化器不会消除该循环。

这个基准显示了像 `set` 和 `map` 这样的树状结构的容器的糟糕表现。令人惊讶的是`vector`显示在优化迭代时间方面比C数组更稳健，似乎$\mathrm{c}$数组需要手动优化以获得其最佳性能。

\textbf{Inserting} 

第一个基准测试显示了将元素推回给不同容器的结果。该测试包括将$10^{4}$元素推回每个容器数次，并测量该操作的平均时间。图[7.44]显示了这个基准测试的结果。

第二个基准显示了`vector`的前推操作及其与容器大小有关的线性复杂性。其他容器具有与推回操作相同的性能，并且如预期的那样明显更快。图[7.45]显示了$10^{4}$对不同大小的向量进行推前的结果。

\textbf{Copying} 

另一个重要的操作是容器的复制。这个基准测试显示了复制不同大小的容器的结果时间。测试包括调用容器的复制构造函数几百次，并计算调用复制构造函数100次所耗费的时间。图[7.46]显示了这个基准测试的结果。

可以看出，`vector`比其他的性能更好，因为它的结构简单，导致复制时间的开销比其他的少。

\textbf{Find} 

在容器中寻找一个元素是另一个需要比较的典型操作。该基准比较了在未排序的容器上的粗暴搜索与在排序的容器上的二进制搜索的性能。第一个测试是在容器中寻找一个值，使用对未排序的 `vector` 和未排序的 `list` 的暴力搜索，对已排序的 `vector` 的二进制搜索以及 `set` 的树状搜索。第二个测试是在未排序的 `vector` 和未排序的 `list` 上使用暴力查找整数密钥，以及 `map` 的树状搜索。图[7.47]显示了这种比较的结果。

这个基准显示了当容器的大小增加时，粗暴的算法变得多么低效。同样可以看出，在排序的`vector`上使用二进制搜索可以导致像`set`那样的树状搜索的相同性能。

图[7.48]显示了同样的结果，但只关注小型容器。这里可以看出，对小容器使用粗暴的算法可以导致比其他算法更好的性能。这个特点将在后面的小数据容器中寻找数值时使用。

\subsection{Designing New Containers} 

可以看出，由于$\mathrm{C}++$语言的静态类型化，标准$\mathrm{C}++$容器是同质的。这意味着使用这种容器来存储双数，如果不把它们分解成`double`的值，就不能重复使用来存储向量和矩阵。这个限制使得开发者不得不为不同类型的数据分离他们的容器或者实现异构的容器。在这一部分，我们介绍了一些能够存储不同类型数据的新容器。

在上一章中，我们介绍了`Variable`基础接口（VBI），并提到了它的优点。在这一部分中，VBI被用来统一不同容器的接口，也为它们提供了一个通用接口，能够存储任何新的变量而不需要重写它们。

\subsubsection{Combining Containers} 

有限元开发人员通常使用整数、实数、向量和矩阵作为他们的数据。在其他一些领域，如电磁配方，也会使用复数。因此，制作一个能够容纳这些数据类型的新容器可以满足大部分的有限元编程需求。实现容纳上述数据类型的`quasi heterogeneous container`的一个非常快速和简单的方法是采取不同的容器并将它们放在一起作为一个新容器。图[7.49]显示了这种容器的一个例子。





\textbf{Interface and Operations} 

\textbf{Access} 为了访问一个元素，这个容器首先要看这个元素的类型是什么，然后访问相应的容器。通常情况下，这种访问包括对一个给定的键的查找过程。然而，通过给容器一个假的顺序，也可以按位置进行访问。

\textbf{Insert} 插入一个新元素就像访问一个现有的元素一样，需要切换元素类型。这个过程只是找到相应的容器并将新元素插入其中,

\textbf{Find} 主要取决于复合容器中的子容器。同样，这个容器只使用数据类型向相应的子容器发送请求，寻找的过程必须由底层容器完成。

\textbf{Erase} 简单地从持有这种类型元素的容器中删除一个元素。这个程序的效率也取决于持有该数据的容器的类型。

\textbf{Iterating} 可以重复使用子容器的相同迭代机制来迭代某种类型的数据。

\textbf{Combining Containers Advantages} 

- 快速和容易实现。标准的容器可以在这里被重用，使实现任务非常容易。

- 非常严格和无错误的结构。没有机会插入错误的数据类型或获得不属于预期类型的数据。一切都依赖于$\mathrm{C}++$静态类型检查，没有危险的类型转换和原始指针操作。

- 将不同类型的数据分开，可以为每一种情况提供更多的专业化服务。例如，在复制的时候，内置类型的容器可以直接在内存中复制，等等。

- 更少的搜索时间，因为每个容器中的数据数量比容器中的数据总数少。换句话说，将容器划分为子容器可以减少搜索的时间。尽管搜索时间在很大程度上取决于子容器的类型。

\textbf{Combining Containers Disadvantages} 

- 为了支持任何新的类型，需要额外的内存开销。正如上一节所提到的，每个容器都有一个内存开销。所以使用更多的容器来保持相同数量的元素会增加每个元素的开销。这个因素可能会对每个容器中非常小的数据数量造成问题。例如，一个用于保存双数、向量、矩阵和复数的容器有一个固定的开销，即8个指针或更多。所以用这个容器来容纳一个`double`和一个`vector`的3个双数至少会造成$100 \%$的内存开销。支持任何新的数据类型仍然会增加这种开销，这可能会导致这个容器在某些问题上无法使用。

- 增加新的类型需要修改容器，尽管这种修改非常小。一个好的实现可以最小化这种修改，但不能使它自动接受任何新的数据类型。这使得它对于一个有未指定使用字段的库来说是不可接受的。

\textbf{Implementation} 

组合容器是一项简单的任务。上一节中描述的任何经典容器都可以在这里用来存储一种类型的数据。没有限制所有类型的容器都是一样的，但通常这是一个很好的选择，因为数据和算法的性质并不依赖于元素的类型。然而在某些情况下，可以使用不同的算法和数据类型的容器来实现特定类型的优化。

如前所述，容器提供了访问、插入、擦除和迭代的接口。尽管这些是标准容器的通用接口，但它们的类型依赖性使它们具有不同的性质。克服这个问题的一个简单方法是为每种类型实现单独的方法。图[7.50]显示了图[7.49]中的容器的访问方法的一个例子，它为每种类型使用了单独的方法。





可以看出，这种接口设计在实现成本上造成了很大的开销。这可以通过使用模板访问方法来避免，该方法有一个表明数据类型的假参数。下面是一个模板访问方法的例子。

~~~C++
template <class TDataType>
TDataType &GetValue(TDataType const &Dummy,
                    ... more data information)
{
    return CorespoundingContainer.Get(...);
}
~~~


使用这个接口意味着引入一个虚拟变量来帮助容器，这并不优雅。在上一章中，我们介绍了VBI，并讨论了它处理不同数据类型的通用方法。现在是时候使用这个概念来为我们的容器创建一个通用的、可扩展的接口了。VBI提供了一个统一的模板模型，用于通过变量来访问一个元素。这个模型使用变量类型参数来区分元素的类型，而不需要假的参数。通过这种方式，访问方法可以用这种新的形式来写。

~~~C++
template <TVariableType>
typename TVariableType ::Type &GetValue(TVariableType const &)
{
    return CorespoundingContainer.Get(...);
}

~~~



变量不仅提供了元素类型的信息，而且还提供了如何通过提供其唯一的索引和名称找到它的信息。这些信息可以被用来寻找元素，而不需要向访问方法传递额外的参数。所以这个访问方法可以用下面的方式来写。

~~~C++
template <TVariableType>
typename TVariableType ::Type &
GetValue(TVariableType const &ThisVariable)
{
    return CorespoundingContainer.Get(ThisVariable.Key());
}
~~~


这种将元素信息封装在一个变量中的做法简化了界面和用户的使用。现在，用户只能通过给出变量代表它来获得一个值，如下所示。

~~~C++
// Without VBI user must know exactly the type of
// data and also its index in container. Here is
// an example of accessing acceleration with index
// 3 in container
acceleration = mData.GetValue(array_1d<double, 3>(), 3);
// Using VBI , user just need to specify the previously
// defined variable
acceleration = mData.GetValue(ACCELERATION);

~~~

VBI还可以防止用户因给出错误的类型或信息而产生琐碎的错误。比如说。

~~~C++
// Error! acceleration type is array_1d <double , 3>
// but given type is std:: vector
acceleration = mData.GetValue(std::vector(3), 3);
// Error! displacement is in position 0 and not 3
displacement = mData.GetValue(array_1d<double, 3>(), 3);
~~~

保持这种形式，我们的异质容器的基本接口可以以下面的形式实现。


~~~C++
// Accessing to a value in container
template <TVariableType>
typename TVariableType ::Type &
GetValue(TVariableType const &ThisVariable);
// Readonly access to the container
template <TVariableType>
typename TVariableType ::Type const &
GetValue(TVariableType const &ThisVariable) const;
// Setting a value in container
template <TVariableType>
void SetValue(TVariableType const &ThisVariable
              typename TVariableType ::Type &Value);
// To see if the variable exist in container
template <TVariableType>
bool Has(TVariableType const &ThisVariable);

~~~




这个接口在这种形式下使用没有问题，但还可以进一步改进。在有限元应用中，有很多情况下需要访问数组或矩阵的某个组件。例如，要给代表位移$Y$分量的自由度赋值，直接访问数据结构中的分量值比提取整个位移`vector`并手动赋值要容易，如下面代码所示。

~~~C++
void UpdataDofValueInContainer(Dof const &rThisDof, double Value)
{
    if (Dof.Variable() == DISPLACEMENT_X)
        mData(DISPLACEMENT)[0] = Value;
    else if (Dof.Variable() == DISPLACEMENT_Y)
        mData(DISPLACEMENT)[1] = Value;
    else if (Dof.Variable() == DISPLACEMENT_Z)
        mData(DISPLACEMENT)[2] = Value;
}

~~~



对组件的直接访问使上述例子变得简单，如下所示。

~~~C++
void UpdataDofValueInContainer(Dof const &rThisDof, double Value)
{
    mData(Dof.Variable()) = Value;
}

~~~


可以看出，第一种手动访问组件的形式并不普遍，只适用于位移组件，而第二种形式可以毫无问题地用于任何定义的组件。因此，我们将为我们的容器实现直接组件访问。

接口必须被修改以区分变量和变量的组件。在VBI中这是一项简单的任务，因为有两个不同的类，`Variable`和`VariableComponent`，代表了它们。所以接口可以通过给定参数的类型来区分一个变量和一个组件。重载每个方法以接受`Variable`或`VariableComponent`，将这两个概念的实现分开。值得一提的是，这种分离是在编译时进行的，不会因为类型检查或其他类型识别机制而产生任何代价。接口的第一部分与获取给定类型的变量和处理它有关。变量的类型由一个模板参数指定，以保持接口的通用性。

~~~C++
// Accessing to a variable in container
template <TDataType>
TDataType &
GetValue(Variable<TDataType> const &);
// Readonly access to a variable in container
template <TDataType>
TDataType const &
GetValue(Variable<TDataType> const &) const;
// Setting a variable in container
template <TDataType>
void SetValue(Variable<TDataType> const &,
              TDataType &);
// To see if the variable exist in container
template <TDataType>
bool Has(Variable<TDataType> const &);
~~~







接口的第二部分由同样的方法组成，这些方法被重载以接受一个`VariableComponent`而不是普通的`Variable`。

~~~C++
// Accessing to component of a variable in container
template <TAdaptorType>
typename TAdaptorType ::Type &
GetValue(VariableComponent<TAdaptorType> const &)

// Readonly access to component of a variable in container
template <TAdaptorType>
typename TAdaptorType ::Type const &
GetValue(VariableComponent<TAdaptorType> const &) const

// Setting a component of a variable in container
template <TAdaptorType>
void SetValue(VariableComponent<TAdaptorType> const &,
              typename TAdaptorType ::Type &);

// Ask if container has this component which usually is
// equivalent to see if the variable holding this component
// is exist.
template <TDataType>
bool Has(Variable<TDataType> const &);
~~~



接下来我们将实现容器。一种方法是用子容器作为其属性来实现容器。图[7.51]显示了这种带有三个子容器的容器的一个例子。

可以看出，对于每个支持的类型，都需要一个接口方法的重载版本来手动调用相应的容器。例如，`GetValue`方法必须为每个类型重载，以调用包含该类型数据的子容器的`GetValue`方法，如下所示。

~~~C++
// Accessing to a double in container
double GetValue(Variable<double> const &rThisVariable)
{
    return mDoubles[rThisVariable.Key()];
}
// Accessing to a Vector in container
Vector &GetValue(Variable<Vector> const &rThisVariable)
{
    return mVectors[rThisVariable.Key()];
}
// Accessing to a Matrix in container
Matrix &GetValue(Variable<Matrix> const &rThisVariable)
{
    return mMatrices[rThisVariable.Key()];
}

~~~









这种手动切换必须用于所有其他处理不同类型数据的方法，如`GetValue` const, `SetValue` , Has和重载操作符，这使得这种实施策略难以维护。

另一种方法是使用多个层次结构，将不同的容器组合在一起。图[7.52]显示了这种用于实现前面例子中的容器的方法。

这种方法与之前的方法最大的区别在于容器的切换机制。在这种方式下，不需要为任何可接受的类型手动重载每个方法。现在，一个模板方法简单地完成了这项工作。这个机制很简单，每个方法都被实现为元素类型的模板，它接受相应的变量并通过调用适当的基类接口自动调用相关的容器。下面是这个实现的一个例子。


~~~C++
class CombinedContainer : public BaseContainer<double>,
                          BaseContainer<Vector>,
                          BaseContainer<Matrix>
{
public:
    /// Default constructor.
    /// Copy constructor.
    CombinedContainer(const CombinedContainer &rOther) : BaseContainer<double>(rOther),
                                                         BaseContainer<Vector<double>>(rOther),
                                                         BaseContainer<Matrix<double>>(rOther)
    {
    }
    template <class TDataType>
    TDataType &
    GetValue(const Variable<TDataType> &rThisVariable)
    {
        return BaseContainer<TDataType>::GetValue(rThisVariable);
    }
};

~~~





现在，支持任何新的变量只需要添加另一个父类，并修改一些其他方法，如复制构造函数，以纳入新的基类。

使用VBI提供的工具可以很容易地直接访问变量的组件。正如前一章中提到的，每个组件都知道它的父变量，也知道如何从它那里提取自己。因此，要提取一个组件，只需要访问父变量的值，并把它交给组件，让它从里面提取自己。下面是一个组件访问方法的例子。

~~~C++
template <class TAdaptorType>
typename TAdaptorType ::Type &
GetValue(const VariableComponent<TAdaptorType> &rThisVariable)
{
    typedef typename TAdaptorType ::SourceType source_type;
    return rThisVariable.GetValue(
        BaseContainer<source_type>::GetValue(
            rThisVariable.GetSourceVariable()));
}
~~~


可以看出，所有的切换和访问算法都是通过模板实现的，这使得它们非常高效。在编译的时候，所有的变量和组件的类型都是已知的，所以编译器可以得到组件提供的转换算法，并将其内联到访问代码中，以消除函数调用的开销。最后，优化器会将所有这些代码减少到适配器提供的直接访问方法，这相当于手写的代码。

通过键查找存储的变量需要在容器中搜索。 `map`提供了一个通过给定的键进行搜索的机制，可以用来寻找任何变量的键而不需要任何实现成本。BaseContainer可以使用`map`来实现，如下所示。

~~~C++
template <class TDataType>
class BaseContainer
{
public:
    typedef map<VariableData ::KeyType, TDataType> ContainerType;
    TDataType &
    GetValue(const Variable<TDataType> &rThisVariable)
    {
        return mData[rThisVariable];
    }

private:
    ContainerType mData;
}
~~~



因此，通过把上述所有的组件放在一起，就可以做出一个带有VBI的容器的最初始版本。这个实现被用于Kratos代码的第一个版本，以开发一个成本最低的可靠容器。

这个容器可以通过将基本容器从 `map` 改为 `vector` 来改进。使用 `vector` 而不是 `map` 有两个主要优势。

- 与 `map` 相比，存储一个 `vector` 的索引数据所需的内存更少。事实上，这个容器将被用来存储节点或元素数据，所以任何内存的减少都会深深影响应用程序使用的整体内存。在上一节中，已经显示了内存使用的巨大差异。对于小型容器`map`需要比`vector`多出大约2到5倍的内存。这个开销可以用 `vector` 完全消除。

- 当容器的大小非常小时，`vector`的搜索时间也比`map`更快，甚至使用暴力手段。同样，由于每个`Node`或`Element`需要存储的数据量很小，所以改成`vector`也可以使搜索过程更快。

实现起来相对容易。每个数据将被存储在一个`pair`中，并与它的密钥相结合。所以要找到任何数据，必须遍历`vector`并将密钥与给定的密钥进行比较。一个叫做`VectorMap`的简单容器可以帮助我们封装所有这些操作，并通过改变标准的`map`来重复使用以前的代码。

\subsubsection{Data Value Container} 

一个`quasi heterogeneous container`可以成功地用于保存类型差异不大的数据。但如果要容纳更多不同的数据类型，异质容器则更有用。数据值容器是一个具有变量基础接口的异质性容器，它被设计用来保存任何类型的变量的值。

通常，一个容器需要对其数据进行一些基本操作。创建、复制和删除就是这种操作的例子。不幸的是，这种操作可能因类型不同而不同。例如，删除`double`可以通过将其从容器中删除来完成，但是删除`vector`可能包括首先释放其内存，然后将其从容器中删除。因此，一个异质的容器需要一种机制来处理每个不同的类型和它相应的进程。例如，通过复制一个`double`的值来复制它，通过调用其复制构造函数来复制`vector`。

处理这个问题的一个常见方法是将所有必要的操作封装到一个处理程序对象中，并将其与相应的数据联系起来。这样，容器就只通过其独特的接口使用这些处理程序来完成不同的任务，而没有任何问题。图[7.53]显示了这种关系。

在我们的方法中，变量被用作处理程序来帮助容器进行数据操作。数据值容器使用`Variable`类不仅可以理解数据的类型，还可以通过其原始指针方法对其进行操作。图[7.54]显示了容器和`Variable`类的关系。

\textbf{Interface and Operations} 

\textbf{Access} `Container`首先使用给定变量的键并找到其值在内存中的位置。然后将这个位置作为变量类型的引用返回。重要的是，类型识别可以在编译时完成，以便在运行时消除其开销。

\textbf{Insert} 插入一个新元素包括分配内存和正确复制对象。找到正确的位置和分配策略在很大程度上取决于数据的内部实现。通过了解数据的类型，通过调用其复制构造函数，可以很容易地完成复制。

\textbf{Find} 主要取决于容器的内部结构，包括搜索容器中变量的键。与复合容器不同，搜索过程不需要类型转换，数据的类型只在返回值的向下铸造时重要。

\textbf{Erase} 删除数据由两部分组成。 首先，使用变量的类型规范调用对象的析构器，然后从内部数据中释放保存值的内存。第一部分是一个简单的任务，因为变量的类型在编译时就已经知道了。第二部分的效率取决于容器的内部数据结构。





\textbf{Copy}复制这个容器不是一个简单的任务，因为复制内存块，或者说浅层复制，对于复制某些类型的数据是不够的。例如，包含一个指针的对象可能需要一个指向性数据的深度拷贝，而不是指针本身。因此，容器逐个元素进行复制，并使用相应的变量来复制数据。这个过程比同质化的容器要慢一些。

\textbf{Clear} 清除包括首先调用每个对象的析构器，然后释放内存。 `Container`使用变量来调用数据的正确的析构器，并正确地从内存中删除它。这个过程是必要的，因为简单地释放内存本身不会调用一个对象的析构器。这导致了一些问题，特别是当对象有一些内部分配的内存时，在没有调用析构器的情况下删除它们会导致系统中的内存泄漏。同样，由于每个元素的函数调用开销，这个过程比同质清算要慢。

\textbf{Data Value `Container` Advantages} 

- 可扩展性，可存储任何类型的数据，没有任何实现成本。为数据值容器添加新的类型是一项自动化的任务，无需改变容器，甚至无需重新配置。人们几乎可以存储任何类型的数据，从简单的数据如整数到复杂的数据如指向邻居元素的动态阵列。

- 通常情况下，大量使用空指针和向下铸造使异构容器容易发生类型崩溃。使用变量基接口可以保护用户免受不必要的类型转换，并保证这个容器的类型安全。

\textbf{Data Value `Container` Disadvantages} 

- 异构容器通常比同构容器慢，至少在它们的一些操作中是这样。类型识别使它们比同质容器的速度慢。

\textbf{Implementation} 

实现这种容器的第一步是设计内存中的数据结构。一种方法是将每个数据与对其变量的引用分组，并将它们放在一个动态数组中，如图所示 [7.55] 。





在这种方法中，元素的迭代在某种程度上就像一个链接 `list` 。对于每一个元素，变量都知道它的大小，因此知道进入下一个元素的必要偏移量。有一个指向每个变量的指针而不是复制它，对于消除重复变量的不必要的开销是必要的。

另一种方法是单独分配每个数据，并保持其在容器中的指针位置。图[7.56]显示了一个在内存中具有这种结构的容器。





第一种方法通常在使用缓存方面更有效率，因为访问一个元素不需要通过指针进行内存跳跃。但是向其中添加新的数据可能会使所有对其元素的引用失效，正如前面提到的动态数组。第二种方法可以让用户获得一次数据的引用，并多次使用，而不必担心其有效性。在这项工作中，第二种方法被使用，因为它在减少对容器的重复访问方面有优势，在实践中可以显著提高其整体性能。

使用这种内存结构，实现起来就比较容易。 首先，一个`pair`对象被用来将变量引用与一个指向其数据的无效指针分组。

~~~C++
// Grouping Variable reference with a pointer to its data
typedef std ::pair<const VariableData *, void *> ValueType;
~~~


现在，内部数据容器可以通过将这些数据对放在一个 `vector` 中轻松实现。

~~~C++
// Type of the container used for variables
typedef std::vector <ValueType > ContainerType;
~~~

为了在这个容器中插入一个新的数据，首先必须创建一个新的`pair`，它持有对其变量的引用和持有该值拷贝的内存位置指针。将这个`pair`添加到`vector`的末尾，就完成了插入过程。

~~~C++
mData.push_back(ValueType (& rThisVariable ,new TDataType(rValue )));
~~~

访问方法使用`Variable`或`VariableComponent`作为数据信息，如VBI所述。每个访问方法包括对给定变量键的查找过程，然后将数据转换为给定的变量类型。

~~~C++
template <class TDataType>
const TDataType &
GetValue(const Variable<TDataType> &rThisVariable) const
{
    typename ContainerType ::const_iterator i;
    i = std::find_if(mData.begin(),
                     mData.end(),
                     IndexCheck(rThisVariable.Key())) if (i != mData.end()) return *static_cast<const TDataType *>(i->second);
}

~~~


如果给定的数据在容器中还不存在，访问方法也可以被配置为返回0，如下所示。

~~~C++
template <class TDataType>
const TDataType &
GetValue(const Variable<TDataType> &rThisVariable) const
{
    typename ContainerType ::const_iterator i;
    i = std::find_if(mData.begin(),
                     mData.end(),
                     IndexCheck(rThisVariable.Key())) if (i != mData.end()) return *static_cast<const TDataType *>(i->second);
    return rThisVariable.Zero();
}

~~~



这里需要提到的是，使用VBI不仅可以增加代码的可读性，还可以保护用户免受不必要的类型崩溃的影响。为了看到这两种方法的区别，让我们做一个不使用VBI的访问方法的例子。


~~~C++
void *GetValue(KeyType Key) const
{
    typename ContainerType ::const_iterator i;
    i = std::find_if(mData.begin(), mData.end(), IndexCheck(Key)) if (i != mData.end()) return i->second;
    return &(rThisVariable.Zero());
}

~~~




同样考虑到应用程序的IO部分从输入中读取一些不同类型的数据，并将它们存储在容器中，如下所示。

~~~C++

// Defining keys
int acceleration_key = 0;
int elasticity_key = 1;
int conductivity_key = 2;
// Reading data
array_1d<double> acceleration;
matrix<double> conductivity;
symmetric_matrix<double> elasticity;
input >> acceleration >> elasticity >> conductivity;
// And store them in data value container
data.SetValue(acceleration_key, acceleration);
data.SetValue(conductivity_key, conductivity);
data.SetValue(elasticity_key, elasticity);

~~~



在代码的其他一些部分，这个变量是必要的，用户将从容器中检索它们，而不指定它们的类型或不同类型。

~~~C++
// The following innocent code simply will not compile!
// Error: There is no * operator which takes a double
// and a void as its arguments
Vector v = delta_time * *data.GetValue(acceleration_key) + v0;
// The following erroneous code compiles without
// problem but crashes in runtime due to the type
// crashing of converting conductivity matrix to a
// double representing the conductivity coefficient!
double k = *(double *)data.GetValue(conductivity_key);
// Again the following code will compile fine but crashes
// mysteriously in run time! Because the elasticity
// was stored as a symmetric matrix
Matrix *d = (Matrix *)data.GetValue(elasticity_key);

~~~



第一条语句计算速度并将其存储为 `Vector` $\mathrm{v}$ 。这条语句看起来没有错误，但是根本无法编译，因为编译器没有任何关于加速度类型的信息。

第二条语句更糟糕，因为它的编译没有任何问题，但不能像预期那样工作。因此，由于这种类型的崩溃，应用程序给出了错误的结果，用户必须进行调试，以找到这个简单的错误。

第三条更错误的语句是将`symmetricmatrix`的指针作为`Matrix`的指针，也会被编译，甚至比第二条语句更糟糕，也可能神秘地工作或不可靠地崩溃，这取决于`Matrix`中内部数据的顺序。

现在让我们看看使用VBI如何通过编译时的类型检查来保护用户免受简单的错误。

~~~C++
// The following code works as it must while the return
// type of GetValue method is a reference to acceleration
// array. So the multiplication can be done correctly.
Vector v = delta_time * data.GetValue(ACCELERATION) + v0;
// The following code will not compile due to the type
// mismatch.
Matrix &d = data.GetValue(ELASTICITY);
// Again the following code will not compile due to the
// type mismatch.
double k = data.GetValue(CONDUCTIVITY);

~~~


第一条语句的编译没有问题，而且也像预期的那样工作。对加速度数组的引用被传递给表达式，速度`vector`将被正确计算。

与之前的方法不同，第二条语句将无法编译，因为编译器无法将symmetric_matrix类型的引用转换为对`Matrix`类型的引用。这个编译中的错误可以保护用户免受不必要的类型崩溃的影响。同时，用户也有可能将弹性`symmetric matrix`正确地复制到普通矩阵中，用于一些后续操作。

最后，第三条语句引起了另一个编译错误，并保护用户免受错误的类型转换。

前面提到的复制容器不能仅仅通过复制内存来完成，因为这种浅层的复制会导致一些对象的不正确的复制，特别是对于那些带有指向其单独数据的指针的对象。例如，让我们考虑一个动态的`vector`，其实现如下:

~~~C++
class Vector
{
    int mSize;
    double *mData;

public:
    // copy constructor
    Vector(Vector &Other)
    {
        mData = new double[Other.mSize];
        memcpy(mData, Other.mData, mSize);
    }
    // access
    double operator[](int i)
    {
        return mData[i];
    }
}

~~~



对这个`vector`的浅层复制将导致`mData`的指针占用源`vector`的`mData`中的地址并指向源数据，如图[7.57] 所示。因此，`vector`vc的元素的任何变化都会改变源`vector`v的元素!

但是使用它的复制构造函数复制相同的 `vector` 将复制分配的内存，并安全地使用 `memcpy` 将源 `vector` 的内容复制到复制的那一个，如图所示 [7.58] 。





数据值容器在复制时使用变量来调用每个元素的复制构造函数，以避免浅层复制产生的错误。这里是复制构造函数的实现。



~~~C++
/// Copy constructor.
DataValueContainer(DataValueContainer const &rOther)
{
    for (ConstantIteratorType i = rOther.mData.begin();
         i != rOther.mData.end(); ++i)
        mData.push_back(ValueType(i->first, i->first->Clone(i->second)));
}
~~~

不幸的是，`Clone`类的`Variable`方法必须是虚拟的，它的函数调用开销使得这个操作比正常的复制要慢。

销毁一个数据值容器也需要谨慎行事，因为释放它的内存会导致内部分配内存的对象的内存泄漏，或者导致一些其他对象的工作未完成。为了避免所有这些问题，在将对象从内存中移除之前，有必要调用对象的析构器。数据值容器使用`Variable`的Delete方法来调用每个对象的析构器，以便正确地删除它们。

~~~C++
void Clear()
{
    for (ContainerType ::iterator i = mData.begin();
         i != mData.end(); i++)
        i->first->Delete(i->second);
    mData.clear();
}

~~~



这个操作在其循环中也包含了一个函数调用，这降低了它的效率。

\subsubsection{Variables List Container} 

在有限元程序中，为一个域的所有`Nodes`存储相同`set`的数据是很常见的。例如，在一个流体域中，每个`Node`都要存储速度和压力。另外，每种类型的`Element`都必须在每个积分点存储特定的`set`的历史数据。以前的异质容器可以用来存储这些数据，因为它可以灵活地存储任何类型的数据。然而，为了访问这个容器中的数据而进行的搜索程序使其效率低下。为了解决这个问题，我们设计了另一个容器，它只存储特定的`set`的数据，但有一个有效的访问机制。

主要的想法是使用一个`indirection`机制来访问容器的元素。一个共享变量`list`给出了每个变量在共享容器中的位置。这个机制非常简单。有一个数组，为容器中的每个变量存储本地偏移量，并为其余变量赋值$-1$。偏移量被存储在变量键的位置，使用零基索引。换句话说，如果一个变量的键是$k$，那么它的偏移量就存储为这个数组的$k+1$'th元素。这个偏移量可以用来通过偏移数据指针来访问内存中的数据。例如，为了在这个容器中找到温度，`TEMPERATURE`变量的键，在这个例子中是2，表示偏移数组的第三个元素包含温度的偏移量是1。然后这个偏移量被用来获取数据数组中的温度值。图[7.59]显示了这个过程。





\textbf{Interface and Operations} 

\textbf{Access} 这个容器首先使用一个给定变量的键，并从变量`list`中获得必要的偏移量。这个偏移量被用来访问它在内存中的值。然后将这个位置作为变量类型的引用返回。重要的是，类型识别可以在编译时完成，以便在运行时消除其开销。如果启用了在容器中插入的功能，有必要进行控制，以查看请求的变量是否真的存储在容器中。

\textbf{Insert} 插入一个新元素包括分配内存和正确复制对象，并将其添加到变量中 `list` 。这意味着将一个新的变量添加到一个容器中，实际上是将它添加到与它共享变量的所有其他容器中 `list` 。在这个容器中使用数组进行数据，意味着对共享变量`list`的任何容器中的元素的所有引用都可以通过插入一个新元素而失效。查找 通过`indirection`进行查找，是一个快速的过程。一个变量键就足以有效地找到它，只需要数据的类型就可以正确地投出搜索的结果。

\textbf{Erase} 删除数据是非常复杂的。它可以通过给`list`中的变量一个删除的标签来完成，然后每个容器必须为新的`list`更新自己。所有这些使得擦除实际上是不可接受的。

\textbf{Copy} 复制这个容器类似于 `DataValueContainer.` 容器再次逐个元素进行，并使用相应的变量来复制数据。这个过程需要一个虚拟函数调用，这降低了它的性能。

\textbf{Clear} 清除这个容器也类似于 `DataValueContainer.` 容器使用变量来调用数据的正确的析构器，并正确地从内存中删除它。这个过程是必要的，因为简单地释放内存不会自己调用对象的析构器。这导致了一些问题，特别是当对象有一些内部分配的内存时，在没有调用析构器的情况下删除它们会导致系统中的内存泄漏。很明显，由于每个元素的函数调用开销，这个过程比同质化的清空要慢。

\textbf{`Variables` List `Container` Advantages} 

- 访问和查找过程是非常有效的，因为只需要两个索引来查找每个值。

- 可扩展性，可存储任何类型的数据，没有任何实施成本。在这个容器中添加新的类型是一个自动化的任务，不需要改变容器，甚至不需要重新配置它。

- 通常大量使用空指针和向下铸造使异质容器容易发生类型崩溃。使用变量基接口可以保护用户免受不必要的类型转换，并保证这个容器的类型安全。

\textbf{Variables List `Container` Disadvantages} 

- 拥有一个共享的变量`list`，就需要付出额外的努力将相关的容器分组，并在不同的组中管理它们。实际上，这使得使用这个容器的对象不那么独立。

- 从这个容器中删除一个变量是一个复杂而困难的任务。由于这个原因，变量`list`容器不能被用于有时间变量的问题中。

\textbf{Implementation} 

实现这个容器的第一种方法是把所有的东西都放在容器里，采取简单的`list`的变量来工作。这种方法看起来很有吸引力，把所有东西都封装在容器里，并使用一个标准的 `vector` 的变量 `list` 。不幸的是，这种设计需要对每次访问的偏移量进行重新计算，这带来了不可接受的开销。因此，让我们改变设计，去除这个不必要的开销。

另一种方法是将该机制分为两部分。一部分用于计算位置，另一部分用于处理内存。在这个设计中，`VariablesList`类保留了要存储的变量的`list`，并且通过为每个变量提供必要的偏移量来提供它们的本地位置。容器负责分配内存、复制自己并使用变量 `list` 以正确的方式清除数据。图[7.60] 显示了这个结构。




这里的一个重要决定是让容器向共享`list`添加新的变量或不添加？这个特性对我们的实现有什么影响？当每个容器能够向存储变量的`list`中添加新的变量时，这个`list`对于所有共享它的其他容器也会发生变化。这种变化意味着每次访问一个容器时，它必须检查 `list` 是否被改变，如果是新的变量，则要更新自己。这个过程在对容器元素的所有访问中引入了一个开销。同时，这种更新会使所有对其元素的引用失效，这使其使用更加复杂，并增加了对其数据的访问次数。

在Kratos中，插入功能被启用，以使该容器与以前的容器兼容。在实践中，问题不仅仅是检查和更新的开销。更新使调试成为一项困难的任务。引用并不可靠，因为不能保证其他容器没有改变 `list` 。在并行计算的情况下，这种情况会更糟糕，因为这种改变可能发生在刚刚获取引用后的另一个线程中。所以最后从这个容器中删除了插入功能以减少使用它的问题。

没有插入功能，这个容器的实现就非常容易。构造是通过分配内存来完成的，其数据大小由一个给定的变量提供 `list` 。 `VariablesList`负责计算所需的内存来存储其变量。为了提高代码的可移植性，内存被划分为一些具有可配置具体大小的块。 `VariablesList`确定存储每个变量所需的块数，并通过所需内存块的总和来计算总大小。构建过程中的一个额外步骤是为元素分配初始值，以避免未初始化的值问题。这可以通过使用给定变量的`AssignZero`方法来完成，该方法将其零值分配给容器中的分配元素。下面的`list`显示了这种类型的容器的默认构造函数。

~~~C++
/// Default constructor.
VariablesListDataValueContainer()
    : mpData(0), mpVariablesList(&msVariablesList)
{
    int size = mpVariablesList->DataSize() * sizeof(BlockType);
    // Allocating data using size provided by variables list.
    mpData = (BlockType *)malloc(size);
    // Initializing elements with zero value given by each
    // variable.
    VariablesList ::const_iterator i_variable;
    for (i_variable = mpVariablesList->begin();
         i_variable != mpVariablesList->end();
         i_variable++)
    {
        std::size_t offset = mpVariablesList->Index(*i_variable);
        i_variable->AssignZero(mpData + offset);
    }
}

~~~







这种容器的复制过程取决于它的源。如果源是空的，这只是意味着清空了这个容器。如果它不是空的，但共享相同的变量 `list` ，就不需要执行元素的去分配和分配程序，这个过程只包括使用变量中的变量进行赋值 `list` 。最后，对于具有不同变量的源 `list` ，有必要清除容器并为新元素重新分配内存。下面的代码显示了赋值运算符。

~~~C++
/// Assignment operator.
VariablesListDataValueContainer &
operator=(const VariablesListDataValueContainer &rOther)
{
    // if the source container is empty call clear.
    if (rOther.mpVariablesList == 0)
        Clear();
    // if other container uses the same variables list
    // assigns the container element by element.
    else if (mpVariablesList == rOther.mpVariablesList)
    {
        // Assigns other elements value using variable ’s assign
        // method.
        AssignElements(rOther);
    }
    else
    {
        // Destruct previous elements by calling their
        // destructors
        DestructElements();
        // Updates variables list
        mpVariablesList = rOther.mpVariablesList;
        // Reallocating the memory for new size
        int size = mpVariablesList->DataSize() * sizeof(BlockType);
        mpData = (BlockType *)realloc(mpData, size);
        // Copying other elements value to new allocated memory
        // using variable ’s copy method.
        CopyElements(rOther);
    }
    return *this;
}


~~~



像之前的容器一样，清除容器包括手动调用每个变量的析构器，然后释放内存。使用适当的变量类成员可以简化这个过程，就像之前看到的那样。

\subsection{Common Organizations of Data} 

在有限元程序中使用了不同的分布数据的方式。每种方式都有其优点和缺点，对某些情况有用，而对其他问题则有困难。本节将解释一些现有的数据分布，并强调它们的特性。

\subsubsection{Classical Memory Block Approach} 

在内存中保存数据的一种旧的标准形式是索引块内存容器。由于老的fortran编译器的限制，老的fortran代码通常使用这种方法。同时一些新的代码仍然在使用它，因为它的性能很好。

在这种方法中，每一类数据都被存储在一个内存块中。数据在这个内存块中的排序取决于使用该数据的算法。一些算法使用一个`Node`或`Element`，对其数据进行处理，然后转到另一个。在这种情况下，每个`Node`和`Element`的数据必须按顺序存储，以便在操作`Node`或`Element`时尽量减少缓存丢失。图[7.61]显示了这种数据的排列。





其他一些算法取一个变量，例如位移，然后对所有 `Nodes` 或 `Elements` 的这个变量执行一些操作。对于这些算法来说，有效的排列方式是将每个变量的值依次存储在不同的`Nodes`或`Elements`中。通过这种方式，缓存缺失最小，而且可以更有效地完成并行计算过程的矢量化。图[7.62]显示了这种数据的排列。





这种排列方式的一个扩展是为单独处理组件的算法分组变量组件，如图[7.63] 中可以看到。

这种结构实现起来很简单，但需要程序员知道每个节点或高斯点的变量的确切数量，还需要知道缓冲区的大小，这在某些情况下可能很难确定。通过掌握所有这些信息，可以为每组实体分配一个内存块，所有的数据都可以很容易地被存储。此外，还可以建立一个矩阵索引系统，以帮助访问一个实体的某个变量，如`Node`或`Element` 。

\textbf{Advantages} 

- 非常容易编程和使用。通常使用索引来访问数据内部（通过一个全局指针），没有额外的指针来分配和删除，所以它很容易创建和使用，也很容易清除。

- 在使用系统缓存时非常快速和高效。把所有的数据放在一起，可以很容易地在运行时保持系统缓存的完整。当我们对所有实体的数据进行循环时，这个优势就更加明显。

- 对于共享内存平台来说，对这些数据结构的操作很容易被并行化。

\textbf{Disadvantages} 

- 该结构对于添加新的变量是刚性的。另外，在不同的`Nodes`或`Elements`中拥有不同的变量，会产生很大的不必要的开销。例如，当我们试图在某个`Nodes`中引入一个新的变量时，我们必须为这个块添加一个新的行（或列）。这意味着我们必须在所有的`Nodes`中为这个变量平均分配内存，无论它们是否有这个变量。这种开销在一些代码中并不大，但对于其他一些代码来说，可能是非常重要的。

- 这个容器是同质的，因此不适合于我们需要一个容器来存储不同的数据类型的情况。

- 添加或删除`Nodes`或`Elements`会导致该数据结构的大量重新分配，这使得它对`Nodes`和`Elements`的数量不断变化的问题不太感兴趣。

\subsubsection{Variable Base Data Structure} 

这是在以前的数据结构上的一个进步。在这种方法中，与每个节点或元素变量相关的数据被存储在一个单独的数组中。例如，该结构有四个数组来存储节点坐标、位移、速度和加速度。因此，在这种方法中，一个变量可以在任何需要的时候被添加、分配或从内存中删除。

在这种方法中，任何对一些实体进行操作的算法不仅要接受一个实体数组作为其参数，而且还需要不同的数据数组作为其操作的需要。这使得方法的输入和输出在代码中更具可读性，但对一些通用方法来说却有更多限制。然而，我们可以创建一个包含所有变量及其名称的表格，并将其作为一个额外的参数传递，以保证方法的可扩展性。

许多fortran代码以及一些$\mathrm{C}$和$\mathrm{C}++$代码使用这种格式来存储他们的数据。

\textbf{Advantages} 

这种容器有很多优点，使它在有限元代码中很受欢迎。

- 在添加新变量或删除现有变量方面具有良好的性能。在这种方法中，创建新的变量或删除一些现有的变量不会改变全局结构，也不会影响数据结构的其他部分。

- 在使用系统缓存方面非常快速和有效，这些算法是面向领域内的变量工作的。

- 能够增加新的类型。这种结构可以用来存储不同类型的数据，没有任何问题。同时，增加新类型的变量也没有任何困难，因为每个变量的数据都存储在不同的数组中，数组中可以有任何类型的数据。

- 易于编程和使用。创建这种数据结构相对容易，与内存块方法相比，要求较少。它也很容易使用，因为访问只是通过一个索引，没有任何重定向或搜索。

- 像之前的方法一样，对这些数据结构的操作可以很容易地在共享内存平台上并行化。

\textbf{Disadvantages} 

- 在从容器中删除一些实体的数据时表现尚可。对于涉及`Node`插入和删除或`Element`插入和删除这种类型的容器的问题会引入大量的开销。为了避免这个问题，我们可以为实体分配一个移除的标志，并更新一次数据结构，然而这种方法也有其复杂性。

- 对于在逐个实体工作的算法中使用不是很有效。在这种结构中，与一个实体相关的不同数据可能被存储在内存中彼此相距很远的地方。这就导致了高速缓存的缺失，从而降低了算法的性能。

\subsubsection{Entity Base Data Structure} 

从抽象的角度来看，我们可以把这个容器看作是变量基础容器的转置结构。在这个结构中，我们把所有与一个实体相关的数据放在同一组中。例如，所有与某个`Node`相关的数据都存储在一起。但与内存块不同的是，不能保证节点数据块按顺序存储在内存中。

一个僵硬但非常快速的实现是使实体的每个变量成为它的成员。通过这种方式，数据的位置性得到了保证，从而提高了应用程序的性能。问题是这种实现的僵化性。每个新的变量都必须在编程时添加到实体中。例如，`Node`必须有应用程序可以解决的不同问题的所有变量。由于这个原因，在多学科代码中不能使用这种方法。图[7.64]显示了这种结构。

另一种方法是为每个实体的相关数据分配一个内存块，并给实体一个对其数据的引用。一个实用的方法是定义一个通用的容器作为每个实体的成员，它可以在那里存储它的数据。这种实现方式更加灵活，但效率较低，因为将数据块从实体中分离出来，会在内存中产生一个跳转来访问它，并产生缓存丢失。图[7.65]显示了这种实现。

使用这种结构，对实体进行操作的算法只需要获取实体的数组。因为每个实体都知道如何访问其数据，所以算法可以从实体中访问其必要的数据，而不需要将其输入和输出作为额外的参数。这就减少了参数的数量，同时保持了代码的可扩展性。

\textbf{Advantages} 

- 在添加一个实体或删除一个现有实体时有良好的表现。与每个实体相关的数据与其他数据分开分组，所以任何与新实体相关的新数据组都可以被添加，而不影响数据结构的其他部分。同样，删除与一个实体相关的数据也可以独立完成，没有任何问题。

- 以这种方式分离数据使得应用程序在分布式内存架构上的并行化更加容易。在这种方式下，没有必要通过索引来找到一个实体的数据。这种独立性对于在机器上划分数据是很方便的。

- 在对实体进行操作的算法中，可以很好地利用缓存。该算法可以访问一个实体的多个数据来完成其操作，然后再进入下一个实体。这种结构具有良好的性能，因为将所有的数据保存在一个内存块中，大大减少了高速缓存的失误。

\textbf{Disadvantages} 

- 在增加新的变量或删除现有的变量时表现公平。在这种方法中，引入新的变量或删除一些现有的变量需要逐个调整数据的大小，这使得它的效率不如以前的方法。

- 在使用系统缓存方面的效率较低，因为这些算法是面向领域内的变量工作的。当从一个实体到另一个实体时，在实体中持有的变量上进行循环会产生内存的跳跃。这使得它在使用缓存内存时效率较低，因此降低了算法的性能。

\subsection{Organization of Data} 

在一个有限元程序中，有几类数据必须被存储。节点数据、元素数据及其时间历史和过程数据是这些类别的例子。同样在一个多学科的应用中，`Nodes`和`Elements`可以存储在代表领域或其他模型复杂性的不同类别。本节将讨论Kratos中数据的全球分布。

\subsubsection{Global Organization of the Data Structure} 

在上一节中，描述了一些在有限元应用中组织数据的常见方法，并讨论了它们的优点和缺点。我们看到，变量库和实体库结构都提供了很好的功能，但是对于两种非常不同的算法。第一种结构对基于领域的算法进行了优化，当一些变量需要从数据结构中添加或删除时，也是如此。而第二种结构更适合于基于实体的算法，在添加或删除实体时更加灵活。

 Kratos的设计是为了支持多学科有限元应用的基于元素的公式，也是以网格适应性作为其目标之一开始的。所以基于实体的数据结构成为最佳选择。 首先，因为元素算法通常是基于实体的，使用这种结构可以更好地进行优化。第二个原因是这种结构提供了良好的性能和灵活性，以便增加或删除 `Nodes` 和 `Elements` 。除了这个实体基础结构Kratos外，还提供了不同层次的容器来组织和分组几何和分析数据。这些容器有助于将所有必要的数据分组，以解决一些问题，并简化在多学科应用中对模型的每个部分应用适当算法的任务。

节点、元素和条件数据容器是这种实体基础结构的基本单位。在Kratos中，每个`Node`和`Element`都有自己的数据。通过这种方式，`Element`只需引用其`Node`，就可以轻松地访问节点信息，而不需要任何复杂的程序。 `Properties`也是这个结构的一个块，作为`Elements`或`Conditions`之间的共享数据。图[7.66]显示了这些基本单元和它们与不同实体的关系。

`Nodes`、`Properties`、`Elements`和`Conditions`的单独容器是 Kratos 中定义的第一层容器。这些容器只是为了将一种类型的实体分组，没有任何额外的数据与之相关。这些容器不仅可以用来处理一组或几组实体，还可以用来修改它们的数据，而每个实体都可以访问自己的数据。当我们想选择一组的实体并处理它们时，这些容器是很有用的。例如将`set`的`Nodes`交给节点数据初始化程序，将`set`的`Elements`发送给组装函数，或者从联系程序中获得`set`的`Conditions`。图[7.67]显示了这些容器和它们的可访问数据。









 `Mesh`是数据结构中的第二层抽象，它持有`Nodes`、`Elements`和`Conditions`以及它们的`Properties`。换句话说，`Mesh`是所有类型实体的完整包，没有任何与之相关的额外数据。因此，`set`中的`Elements`和`Conditions`以及它们的`Nodes`和`Properties`可以被分组为一个`Mesh`，并被发送到诸如网格细化、材料优化、网格移动或任何其他对实体工作的程序，而不需要额外的数据。图[7.68]显示了`Mesh`与它的组件。

下一个容器是`ModelPart`，它是一个完整的`set`数据结构中的所有实体和所有类别的数据。它持有 `Mesh` 和一些被称为 `ProcessInfo` 的额外数据。任何与模型的这一部分有关的全局参数或与进程有关的数据，如时间步长、迭代次数、当前时间等，都可以存储在 `ProcessInfo` 中。 `ModelPart`也管理着`Nodes`、`Elements`和`Conditions`中持有的变量。例如，所有属于一个`ModelPart`的`Nodes`共享节点变量`list`由它持有。从另一个角度来看，`ModelPart`是多学科有限元方法中最接近领域概念的容器。图[7.69]显示了`ModelPart`与它的组成部分。

在最初的实施中，`ModelPart`能够保持数据的历史，如果数据在变化，也能够保持`Mesh`。但在实践中，这种能力成为Kratos性能的瓶颈，而且被认为对我们的问题没有必要。所以这个功能在`ModelPart`中被移除。然而，每个`ModelPart`仍然可以容纳一个以上的`Mesh`，它来自于第一个实现，可以用来代表并行计算中的分区。

最后，`Model`是一组`ModelPart`，表示要分析的有限元模型。




它对一些需要整个数据结构的程序很有用，如保存和加载程序。由于 Kratos 中的程序使用 `ModelPart` 作为它们的工作域，这个容器还没有实现，但它对于完成 Kratos 的数据结构是必要的。

空间容器是分离的，所以可以在需要的时候才使用。这种策略也允许 Kratos 使用实现一般空间容器的外部库，如 `Approximate Nearest Neighbor` （ANN）库 [^74] 。

\subsubsection{Nodal Data} 

Kratos的第一个实现有一个数据值容器的缓冲区来容纳所有节点变量。这种节点容器非常灵活，但对于非历史变量来说，有相当大的内存开销。例如，为了保存历史上的两个时间步骤（一个大小为3的缓冲区），所有非历史变量在内存中有两个冗余的副本，如图所示 [7.70] 。由于容器的搜索过程，它也有公平的访问性能。所有这些使我们重新设计了数据的存储方式 `Nodes` 。

新的结构被分为两个不同的容器：节点数据和解决步骤的节点数据。图[7.71] 显示了这种节点数据结构。一个数据值容器用于节点数据（无历史数据），一个变量 `list` 容器用于解步骤节点数据（历史数据）。通过这种方式消除了内存开销，因为没有产生多余的拷贝。同时，由于在变量`indirection`容器中的访问过程，而不是在数据值容器中的搜索过程，对历史变量的访问要比以前快得多。这种结构提供了很好的性能，也很节省内存，但稳定性稍差，而且在某种程度上使用起来不太灵活。





对历史变量使用变量`list`容器，需要用户定义其历史变量，以构建节点数据容器。例如，一个流体应用必须在程序启动时定义速度和压力作为其历史变量。其余的变量可以在程序执行过程中随时作为无历史变量加入，但不能作为历史变量加入。

这个结构的第一次实现是通过创建一个变量的缓冲区`list`容器来完成的，以减少执行任务，并在一个实际问题中进行测试。在获得成功的结果后，是时候对其进行更多的优化了。变量的缓冲区`list`容器在内存中产生了几个跳转，这降低了它的缓存效率。另外，一些网格生成器，如`TetGen`[^93][^7]需要一个节点数据的数组作为参数来进行插值。由于这些原因，在当前的结构中，缓冲区被移到了解决步骤的容器中。这样就减少了缓冲区的缺失，而且数据阵列可以不经任何转换就交给其他应用程序。图[7.72]显示了这种结构。





在第一个结构中，所有的数据都存储在同一个容器中。所以有一个地方可以存储它们，也有一个地方可以恢复它们。将节点数据分为两类也改变了数据的访问界面。现在，用户必须知道把每个变量放在哪里，更重要的是，以后在哪里检索它们。为了对这两类数据提供清晰和完整的控制，需要一个复杂的接口。一般来说，有三种类型的访问方法是必要的。

- 仅用于访问历史数据的方法。这些方法保证当且仅当它被定义为历史变量时才会给出变量的值，如果它没有被定义则会产生错误。这些方法非常快，因为它们不需要在数据值容器中搜索，也会在代码中出现逻辑错误时产生错误。

- 只访问没有历史数据的方法。另一组的方法被实现，只给无历史数据的访问。由于数据值容器的灵活性，任何变量都可以使用这些方法在任何时候被添加为节点变量。

- 混合访问也可以用另一种`set`的方法来完成。这些方法试图在解决步骤容器中找到该变量，如果它不存在，则提供对节点数据容器的访问。这些方法有助于访问一些输入变量，这些变量可能来自于作为节点数据的输入文件，或者是在另一个领域计算并存储为解决步骤变量的变量。例如，结构问题的温度可以是来自输入数据的参数，也可以是由热元素计算并存储在 `Nodes` 中的参数。这种方法保证了对存储在每个 `Node` 的适当温度的访问。

以下方法是为了提供上述访问节点数据的方式。

\textbf{GetSolutionStepValue} 获取一个变量或一个变量的分量和解步索引，如果存在，则在解步数据中返回其值，否则发送错误。解决步骤的索引从0开始，用于当前步骤，然后增加用于过去的步骤。例如，1代表前一步，2代表前一步之前的一步，等等。一个重载版本只接受变量或变量的组成部分，并返回其当前值。在所有的重载版本中，访问一个没有在解决步骤变量中声明的变量`list`会产生一个异常。

\textbf{GetValue} 这个方法有不同的重载版本。一个以请求的变量为参数的方法，提供了对节点数据的访问，而不需要查看解决步骤的数据。给出一个求解步骤的索引作为额外的参数，使其寻找节点数据，如果它不存在，就从求解步骤容器中获取它。它将在节点数据结构中为给定的变量找到任何现有的值，但是在数据中搜索使得它在访问解决方案步骤数据时很慢。

\subsubsection{Elemental Data} 

Kratos数据结构的另一个基本单元是元素数据。元素数据被分为三个不同的类别。

\textbf{properties} 所有可以在 `Elements` 之间共享的参数。通常材料参数是`set`的`Elements`共同的，所以这类数据被称为属性。但一般来说，它可以是一组`Elements`的任何共同参数。将这些数据作为属性共享可以减少应用程序使用的内存，也有助于在必要时更新它们。

\textbf{data} 与一个`Element`相关的所有变量，并且不需要保留历史。分析参数和一些输入是基本的，但不需要保留它们的历史。这些变量可以在分析过程中随时添加。

\textbf{historical data} 所有存储的数据都有可能需要被检索的历史信息。集成点中的历史数据就属于这一类别。这些数据必须以特定大小的缓冲区来存储。

如上所述，`Properties`是在`Elements`之间共享。出于这个原因，`Element`保持一个指向其`Properties`的指针。这种连接允许几个 `Elements` 使用同一个 `Properties` 。一个 `DataValueContainer` 在 `Element` 中不持有任何历史数据。使用DataValueContainer提供了灵活性和稳健性，这在将元素数据从一个领域转移到另一个领域时非常有用。重要的是，这些无历史数据不是分析过程中使用最多的，这里的灵活性比性能更关键。相反，历史数据在分析过程中使用得更多，获取这些数据的效率比它们的灵活性更重要。这些数据是由配方指定的，其他过程不会改变它们。由于这个原因，`Element`没有为它们提供任何容器，这些容器可以由元素开发者实现。通过这种方式，定制的容器将更加有效，任何通用容器的开销将被消除。

\subsubsection{Conditional Data} 

条件数据与元素数据非常相似，也被分为三个不同的类别。


\textbf{properties} 至于`Elements` ，所有可以在`Conditions`之间共享的参数被称为属性。同样，作为属性共享这些数据可以减少应用程序使用的内存，也有助于在必要时更新它们。

\textbf{data} 与`Condition`有关的所有变量，并且没有历史记录。每个`Condition`的分析参数和一些输入是不同的，但不需要保留其历史记录。这种变量可以在分析过程中随时添加。历史数据 所有存储有`set`的数据其历史都可以检索到。集成点中的历史数据就属于这一类。这些数据必须用一个特定大小的缓冲区来存储其先前的值，以便以后使用。

 像\textbf{Condition}一样，`Element`保留一个指向其`Properties`的指针，该指针被其他`Conditions`或`Elements`共享。

`Condition`有一个DataValueContainer作为其成员，以保持与`Condition`有关的所有数据，而不保留其历史。任何从这个类派生出来的 `Condition` 都可以使用这个容器来保存它的数据，而不需要任何额外的实现。这个基类还为这些数据提供了一个标准接口，这使得它有助于将一些数据从一个`Condition`转移到另一个`Condition`，例如在两个域之间的交互中。

对于`Conditions`来说，历史数据被认为是内部数据，因为它与配方非常相关，通常只被内部配方使用，而不是来自外部。因此，为了尽量减少不必要的开销，也为了提高性能，没有为历史数据提供通用的容器，如果有必要，每个`Condition`必须为自己实现一个。

\subsubsection{Properties} 

如前所述 `Properties` 是 `Elements` 或 `Conditions` 之间的一个共享数据容器。在有限元问题中，有几个参数对于 `set` 的 `Elements` 和 `Conditions` 都是相同的。热导率、材料的弹性和流体的粘性就是这些参数的例子。 `Properties` 保存这些数据，并由 `Elements` 或 `Conditions` 共享。这消除了由于每个`Element`和`Condition`的这些数据的冗余拷贝而产生的内存开销，正如在图[7.73]中可以看到的。





可以看出，改变`Properties`中的任何数据都会影响所有共享它的`Elements`或`Conditions`。在分析过程中，当一个共同的参数发生变化时，这个功能也会很有用。该参数只能在 `Properties` 中改变，每个 `Element` 或 `Condition` 将通过访问 `Properties` 获得新值。这样一来，就不需要在每次数据变化时更新所有的元素和条件值。 如果有必要，`Properties`也可以用来访问节点数据。值得一提的是，通过 `Properties` 访问节点数据与通过 `Node` 访问节点数据是不同的。当用户向 `Properties` 询问 `Node` 中的变量数据时，该过程首先在 `Properties` 数据容器中找到该变量，如果它不存在，则从 `Node` 获取。这意味着数据的优先级是存储在 `Properties` 中的，然后是 `Node` 中的。例如，考虑到`TEMPERATURE`作为材料温度存储在`Properties`中，其值为$24.4$，同时还有一个`TEMPERATURE`节点数据存储在`Node`中，其值为$79.3$ 。现在从`Properties`中获取`TEMPERATURE`变量的值，得到$24.4$，而从`Node`中获取它，得到$79.3$ 。

\subsubsection{Entities Containers} 

让我们在Kratos的数据结构中再上一层。下一层由四个实体容器组成。

- `Nodes` `Container`。

- `Properties` `Container`。

- `Elements` `Container` 。

- `Conditions` `Container`。

在有限元程序中，有几个程序可以接受一组实体并对其或其数据进行操作。这些容器的创建是为了帮助用户将一组实体分组并对其进行操作。例如，将边界内的所有`Nodes`放在一个`Nodes`容器中，并在每一步中改变它们的一些数据。如前所述，每个实体都可以访问它的数据，所以在一个容器中拥有一组的实体也可以访问它们的数据，这使得这些容器在实践中更加有用。

这些容器的另一个用途是通过其索引找到一个实体。索引是识别有限元程序中实体的标准方法。例如`Nodes`有它们的索引，可以通过它们来识别。每个`Node`的索引是由用户作为输入给出的，可以是连续的，也可以不是。用户以后将使用这些指数来定义元素的连接性。在创建`Element`时，有必要找到具有给定索引的`Node`，并将其指针指向`Element`。支持索引系统和提供搜索机制对于以简单和有效的方式实现这样的过程非常有用。

根据前面提到的所有用途，一个合适的用于容纳实体的容器必须提供以下特征。

\textbf{Sharing Entities} 在某些情况下，一个实体可能属于多组的实体。例如，一个边界`Node`属于所有`Nodes`的`list`，也属于边界`Nodes`的`list` 。因此，`Nodes`容器必须与其他`Nodes`容器共享它的一些数据。一般来说，共享实体是这些容器的一个重要特征。

\textbf{Fast Iterating} 如前所述，这些容器的一个重要用途是收集一些实体并将它们传递给一些程序。通常，这些程序必须在给定容器的所有元素上进行循环，并在一些算法中使用每个元素或其数据。因此，这些容器必须提供一个快速迭代机制，以减少逐个元素迭代的时间。

\textbf{Search by Index} 在有限元程序中，通过索引寻找实体是一项常见的任务。所以实体容器必须提供一个有效的搜索机制，以减少这些任务的时间。


共享实体是容器要提供的第一个功能。持有指向实体的指针而不是实体本身可以解决这个问题。不同的列表可以毫无问题地指向同一个实体。使用`smart pointer`[^37]而不是普通的指针可以增加代码的健壮性。这样一来，不再属于任何`list`的实体就会自动从内存中删除。因此，实体的智能指针的容器是这个目的的最佳选择。

如上所述，数组在迭代的时候非常有效。因此，使用数组来保存指向实体的指针可以提高迭代速度。相反，树的迭代速度非常慢，但通过索引搜索时非常有效，因此使用它们可以提高代码的搜索性能。解决这一冲突的一个好办法是有序数组。它像数组一样快速迭代，也像树一样快速搜索。它唯一的缺点是它不太健壮，而且根据数据的不同，构建它可能需要相当长的时间。例如，构建一个`Nodes`与`Nodes`的反序数组可能需要很长的时间。幸运的是，大多数情况下，实体是以正确的顺序给出的，这就消除了这些容器的构建时间开销。另外，在最坏的情况下，无序数据的缓冲区可以大大减少构造开销。因此，一个有序的数组可以适当地适应我们的问题。

 `PointerVectorSet`是一个指向实体的有序数组的模板实现。这个模板被用来创建不同的容器来容纳不同类型的实体。

\subsubsection{ Mesh } 

Kratos '数据结构的下一层是 `Mesh` 。它包含了之前提到的所有实体容器。这种结构使它成为处理不同实体及其数据的程序的良好论据。例如，一个优化程序可以把一个`Mesh`作为它的参数，并改变几何形状、节点数据或属性。 `Mesh`是一个容器的容器，有一个大的界面，帮助用户分别访问每个容器。

 首先，所有的`Mesh`为其存储的每种类型的实体提供单独的接口。[表7.1]、[表7.2]、[表7.3]和[表7.4]显示了其处理不同组件的接口。

 `Mesh`持有一个指向其容器的指针。这样，几个Meshes可以共享例如一个`Nodes`或一个`Elements`的容器。这有助于在多学科应用中更新不同领域的Meshes，但在同一领域。图[7.74]显示了Meshes之间共享组件的这种能力。





\subsubsection{Model Part} 

 `ModelPart`是以两个不同的任务为基础创建的。第一个任务是封装 Kratos 的所有实体和数据类别，这使得它在 Kratos 中作为全局程序的一个参数很有用。第二个任务是管理其组件的变量列表。

 `ModelPart` 可以容纳任何类别的数据和所有类型的实体 Kratos 。它可以容纳几个Meshes。通常只有一个`Mesh`被分配给它并用于计算，然而这种能力可以有效地用于分割模型部分并将其发送到并行进程中。除了容纳不同的Meshes，它还存储封装在`ProcessInfo`对象中的解决方案信息。图[7.75] 显示了 `ModelPart` 的结构。

 `ProcessInfo`不仅保存了不同解法参数的当前值，而且还保存了它们的历史。它可以用来保存变量，如时间、求解步骤、非线性步骤，或任何其他在 Kratos 中定义的变量。它的变量基础接口提供了对这些数据清晰而灵活的访问。 `Process` 信息使用链接的 `list` 机制来保持其历史，如图[7.76] 所示。

 `ModelPart` 使用指向其Meshes的指针。通过这种方式，如果有必要，它可以与任何其他模型部件共享它们。这个功能的一个典型用途是在同一个Meshes上定义两个不同的域。









Meshes。图[7.77]显示了这种共享机制。





 `ModelPart`管理其部件的变量列表。在第7.3.3节中，已经描述了变量`list`容器的机制，其中我们还提到，共享变量`list`规定了可以存储在其中的数据。 `ModelPart`为其所有实体持有这个变量`list`。换句话说，属于一个模型部分的所有实体共享同一个`list`的变量。例如，`ModelPart`中的所有`Nodes`可以在其解决步骤容器中存储相同的`set`变量。值得一提的是，这个变量`list`被分配给属于模型部分的实体，当该模型部分与其他模型部分共享时，这些变量不会改变。图[7.78]显示了这个方案。





\subsubsection{Model} 

 `Model`是通过有限元分析的整个物理模型的表示。定义`Model`的主要目的是完成数据结构中的抽象层次，以及一个收集所有数据的地方，同时也持有全局信息。这个定义使得它对于执行全局操作如保存和加载非常有用。它持有对模型部分的引用，并提供一些全局信息，如实体的总数等。可以看出，在给定模型上创建的`ModelPart`可以自己完成大部分的这些操作。这就是`Model`本身还没有被用于Kratos的原因。它的实际用途是在实现序列化过程或其他全局操作时。


\section{Finite Element Implementation} 

\subsection{Elements} 

 `Elements` 和 `Conditions` 是 Kratos 的主要扩展点。通过实现新的 `Element` 及其相应的 `Conditions` ，可以将新的公式引入 Kratos 。这使得`Element`成为我们设计中的一个特殊对象。

\subsubsection{Element's Requirements} 

一个`Element`被用来为Kratos引入一个新的表述。为了保证Kratos的可扩展性，增加一个`Element`必须是一个简单的任务，并且不需要对代码进行大量的修改。为实现这一目标，需要将计算本地矩阵和结果所需的所有数据和程序封装在一个对象中，并使用一个清晰的接口。

用户可以对模型的不同部分使用不同的`Elements`，然后将它们单独或组合起来。例如，在为一个多楼层结构建模时，用户将使用梁元素作为框架，使用壳元素作为楼层。所以这些`Elements`必须兼容，才能作为一个复杂的系统一起解决。由于这个原因，在任何部分使用任何`Element`的能力，甚至混合它们，是在Kratos设计中要考虑的另一个要求。

 `Element`必须有一个非常灵活的界面，因为有各种各样的配方和不同的要求。例如，有些公式需要在每个求解步骤中计算刚度矩阵和右侧的`vector`。其他一些只需要计算一次刚度矩阵和每一步的右手边。有时需要单独拥有阻尼矩阵来处理不同的时间依赖策略。这些公式不仅在局部矩阵上有所不同，而且它们还需要不同的数据来计算。例如，有些需要时间和时间步长，有些需要非线性迭代次数，或者其他参数，这取决于所选择的分析策略。

易于实现是对一个 `Element` 的另一个要求。有限元开发人员通常不太熟悉高级编程语言的功能，他们希望更专注于他们的有限元开发任务。出于这个原因，设计Kratos的主要意图是将这部分内容与内存工作或对模板的过度使用隔离开来。我们的想法是为`Element`提供一个清晰和简单的结构，以便希望在Kratos中引入新公式的有限元开发人员能够实现。性能是在设计`Element`时需要考虑的一个非常重要的问题。在有限元代码`Elements`中，几乎大部分代码的内循环都会调用方法。这意味着`Element`的性能中的任何小故障都会导致程序执行时间的巨大开销。很明显，一个新的`Element`的性能在很大程度上取决于它的实现，但有时一个弱的设计会导致所有`Elements`的性能出现严重瓶颈。我们将在后面看到一个优雅但不优化的接口如何高度降低 `Elements` 的性能。

内存效率对于 `Elements` 也很重要。用有限元建模通常需要创建大量的`Elements`来解决一个实际问题。由于这个原因，每个`Element`所使用的内存的任何不必要的开销都会导致程序使用的整个内存的重大开销。顺便说一下，这种开销会限制在一台机器上可以分析的模型的最大尺寸。因此，内存的效率被认为是非常重要的，不那么重要的功能必须减少，以保持`Element`尽可能小。

\subsubsection{Designing Element} 

在审查了`Element`的要求后，下一步是设计它。在Kratos中，`Element`是一个持有其数据并计算元素矩阵和向量的对象，以进行组装，也可用于分析后计算局部结果。例如，热元素计算局部刚度矩阵和质量矩阵（如果需要），并将其交给Kratos用于`assembly`过程。同时，它也可以在解决问题后用于计算热流。这个定义为`Element`提供了一个与其他代码相关的良好隔离，这对`Element`的正确封装是有帮助的。

 `Elements`必须被设计成可以独立实现，并且很容易添加到Kratos中，以保证Kratos的可扩展性。同时，它们必须相互兼容，以便让用户互换，甚至将它们混合在一个复杂的模型中。根据这两个要求，第3.4.1节中描述的策略模式就是我们要寻找的。将这种模式应用于我们的问题，就会产生图`Elements`中所示的$8.1$'结构。





使用这种模式，每个`Element`都单独封装了一种算法，也使它们可以按照我们的要求进行互换。 `User`保持一个指向`Element`类的指针，该指针可以指向`Element`家族的任何成员，并使用`Element`的接口来调用不同的程序。

`Elements`设计中的下一个概念是它与几何的关系。如$5.3$节所述，几何体持有`set`点或`Nodes`，并提供`set`的通用操作以简化`Elements`和`Conditions`的实现。每个`Element`都必须与几何体打交道，从很多角度来看，它是一个扩展的几何体，有一个有限元公式，因为它是扩展部分。这种关系在面向对象的哲学中可以转化为父类和派生类的关系，如图[8.2] 所示。





这种结构的优点是`Elements`对几何数据的访问是快速的，提高了元素程序的性能。除了这个优点外，还有两个主要的缺点，使这种结构不适合我们的目的。第一个缺点是，将一个公式应用于不同的几何体，需要实现几个`Elements`。第二个缺点是不同配方的`Elements`不能在内存中共享一个几何体。

在这个结构中，每个`Element`都可以被实现，以在衍生的几何体上增加一个配方。所以必须实现不同的`Elements`来扩展一个公式到不同的几何体。例如，一个三角形的平面应力元素是由一个三角形派生出来的，并且有一个平面应力公式。将同样的公式应用于四边形需要编写另一个`Element`，其结构几乎相同，但来自四边形。这导致在实施和维护`Elements`方面的巨大开销。

在一个多学科问题中，有一些情况是两个相互作用的领域使用同一个网格。为了实现这种可能性，不同的`Elements`应该能够共享同一个几何体。通过这种方式，数据传输最小化，插值成本也消除了。一个简单的例子是热和结构的相互作用。 `Mesh`被用来创建热元素并计算域上的温度。然后用相同的网格创建结构元素，并使用之前计算的温度计算域内的应力和变形，以获得与温度有关的材料。在不共享几何体的情况下，必须创建所有几何体的副本，这样每组的几何体都可以分配给一个域。这增加了应用程序使用的内存，通过共享网格中的几何体可以很容易地避免。

另一种设计是使用一个`bridge`模式。在我们的`Element`的结构设计中引入这种模式的结果是图[8.3]中所示的结构。





这种模式允许每个`Element`将其配方与任何几何图形结合起来。这样一来，就需要更少的实现。同时，有一个指向几何体的指针允许一个`Element`与其他的几何体共享。这种结构的唯一缺点是内存中的指针重定向带来的时间开销。与直接派生的`Element`相比，在派生的几何体旁边有一个指向几何体的指针会在访问几何体的数据时产生少量开销。尽管`Element`中的效率是至关重要的，但第一种方法的复杂性使我们不得不接受性能上的微小差异，因此我们已经实施了第二种方法。一个更好的解决方案是将`Element`作为其几何形状的模板。使用模板可以提供良好的性能和足够的灵活性，但它被认为太过复杂，无法被有限元用户所使用。如前所述，`Element`必须易于编程，并具有较少的编程语言的高级功能。因此，最终选择了目前带有`bridge`模式的结构。

有一些设计，不同的`Elements`可以组成一个更复杂的`Element` [^70] 。这里可以用一个`Composite`模式来模拟这种方法。然而这种结构在 Kratos 中还没有实现。

在设计了全局结构之后，现在是定义接口的时候了。这里的有限元方法有助于设计一个通用的接口。根据有限元程序，该策略要求`Element`提供其局部矩阵和向量，以方程ID的形式提供其连接性，并且在求解后还调用`Element`来计算元素的结果。所以`Element`必须提供三组的方法。

\textbf{Calculate Local System} 第一组的方法需要计算局部矩阵和向量。

\textbf{Assembling Information} 这些方法给出了本地系统的每一行和每一列在全球系统中的位置信息。这些信息来自于自由度，`Element`通过给出其自由度或只是其方程ID来提供。

\textbf{alculate} 它被用来计算与`Element`相关的任何变量，这些变量通常是取决于元素内部梯度的结果。

这里的一个重要问题是这些方法的效率。一个有吸引力的形式是使这些方法将其必要的参数作为参数，并将其结果作为返回值。

~~~C++
Matrix CalculateLeftHandSide(ProcessInfo &rCurrentProcessInfo)
{
    // calculating stiffness matrix
    return stiffness_matrix;
}
// Assembling
for (int i = 0; i < number_of_elements; i++)
    Assemble(elements[i].CalculateLeftHandSide(process_info));

~~~



可以看出，这种设计是非常自然和容易使用的，但在实践中会产生很大的性能开销。调用每个方法包括创建一个新的矩阵或`vector`，填充它，最后把它的值作为结果传给它。创建一个动态矩阵或`vector`是一个非常缓慢的过程，而且通过数值传递需要创建临时变量，这很耗费时间。所有这些步骤使得这个设计非常缓慢，因此是不可接受的。一个更好的想法是将结果矩阵或`vector`通过引用作为附加参数传递给这些方法。这样一来，就没有临时性的按值传递，也不需要在每个计算方法里面为结果创建一个变量。这种新设计的一个性能问题是结果矩阵和向量的大小调整。在实践中，调整动态矩阵和向量的大小会非常慢。在调整大小之前，对给定的矩阵或`vector`大小进行简单的控制可以减少给定大小正确的情况下的调整开销。

一套计算局部系统矩阵和向量的方法是必要的。有限元方法中的不同程序在不同的分析点需要不同的信息，从 `Element` 。例如，一个简单的线性策略需要一次局部矩阵和向量来组装全局系统。所以一个计算局部系统分量的方法就足以处理这个策略。但是对于非线性分析，策略需要同时得到右手边来计算收敛。在这些情况下，计算这个右手边分量的方法是必要的。还有一些情况，在分析过程中，右手边没有变化，策略只需要更新其左手边的分量，这就需要一个只计算左手边分量的方法。

只实施第一种方法来计算局部系统分量会导致非线性情况下的计算开销。例如，计算具有残差准则的解决方案的收敛性，只需要更新右手边的分量，计算所有的分量会给这个程序带来不可接受的开销。只保留左右两边组件的接口也会产生计算开销。通常，为了计算本地系统的每一部分，必须计算元素的雅各布式，这是一个耗时的操作。分别计算右边和左边的组件意味着必须计算两次雅各宾（jacobian）。所以一个最佳的设计是保持两个界面的并行。

这个决定的缺点是需要实现重复的方法。这个问题可以通过一个更仔细的实现来解决。我们可以创建两个私有的辅助方法。 `LeftHandSide`和`RightHandSide`来计算左手边和右手边的矩阵和向量，并将计算出的jacobian作为其输入。然后`CalculateLocalSystem`可以计算一次jacobian，并用这个jacobian调用这些方法来计算局部系统组件。另外`CalculateLeftHandSide`和`CalculateRightHandSide`也会计算出雅各宾，并调用其相关方法来计算局部贡献。下面是这个实现的一个例子。

~~~C++
class MyElement
{
public:
    virtual void
    CalculateLocalSystem(MatrixType &rLeftHandSideMatrix,
                         VectorType &rRightHandSideVector,
                         ProcessInfo &rCurrentProcessInfo)
    {
        Matrix jacobian;
        Jacobian(jacobian);
        LeftHandSide(rLeftHandSideMatrix, jacobian,
                     rCurrentProcessInfo);
        RightHandSide(rRightHandSideVector, jacobian,
                      rCurrentProcessInfo);
    }
    virtual void
    CalculateLeftHandSide(MatrixType &rLeftHandSideMatrix,
                          ProcessInfo &rCurrentProcessInfo)
    {
        Matrix jacobian;
        Jacobian(jacobian);
        LeftHandSide(rLeftHandSideMatrix, jacobian,
                     rCurrentProcessInfo);
    }
    virtual void
    CalculateRightHandSide(VectorType &rRightHandSideVector,
                           ProcessInfo &rCurrentProcessInfo)
    {
        Matrix jacobian;
        Jacobian(jacobian);
        RightHandSide(rRightHandSideVector, jacobian,
                      rCurrentProcessInfo);
    }

private:
    void LeftHandSide(MatrixType &rLeftHandSideMatrix,
                      Matrix &rJacobian,
                      ProcessInfo &rCurrentProcessInfo)
    {
        // Calculating left hand side matrix using given jacobian.
    }
    void RightHandSide(VectorType &rRightHandSideVector,
                       Matrix &rJacobian,
                       ProcessInfo &rCurrentProcessInfo)
    {
        // Calculating right hand side vector using given jacobian.
    }
};


~~~









还有一点很重要的是，`Elements`不一定要实现所有这些接口，它们可以只兼容一种方式而不提供另一种。顺便说一下，在`Element`类的`CalculateLocalSystem`方法中调用两个独立的方法，可以使不提供`CalculateLocalSystem`方法的`Elements`保持更多的兼容性，而只是提供`CalculateLeftHandSide`和`CalculateRightHandSide`方法。

另一个问题是对对称或对角线矩阵的优化。在Kratos中，局部系统矩阵被定义为密集矩阵，以便更加通用。 `Elements`中的对称公式也要填充这种密集矩阵。可以在策略层面上进行优化，在对称全局矩阵中只装配这个矩阵的一半，以减少内存的使用，也减少装配时间。然而，为了保持`Elements`与非对称策略兼容，填充密集矩阵的所有组件的冗余时间是不可避免的。 `Diagonal` 矩阵可以被视为对称的，只要在策略中保持优化水平，而不是在 `Element` 中。

这个接口被设计成通用的，但是它支持新算法的灵活性也取决于它在传递不同公式所需的不同参数的能力。出于这个原因，使用了一个变量基础容器，使用户能够使用前面描述的VBI向`Element`传递任何参数。 `ProcessInfo`可以用来传递任何在`Element`中计算本地系统所需的参数。通常的参数是时间、时间增量、时间步长、非线性迭代数、一些在域上计算的全局准则等。使用`Process`信息保证了灵活性，这对于`Element`成为Kratos的扩展点是必要的。

根据前面的评论，设计了以下方法。

\textbf{CalculateLocalSystem} 这个方法计算所有的局部系统组件。它需要一个左手边的矩阵和一个右手边的 `vector` 将其结果放在其中, `ProcessInfo` 被传递以提供分析参数。

\textbf{CalculateLeftHandSide} 这个方法只计算左手边的矩阵。它需要一个矩阵来将其结果放入其中,传递Processinfo以提供分析参数。 `ProcessInfo` 它提供分析参数。

\textbf{CalculateRightHandSide} 计算本地系统的右手边分量。它需要一个`vector`来把它的结果放在里面。 `ProcessInfo` 被传递以提供分析参数。

 `Element`还必须为`Strategy`提供装配信息。它必须提供每个局部系统的行和列在全局方程系统中的相应位置。 然后`Strategy`使用这些信息在全局方程组中正确组装本地矩阵和向量。该信息来自与本地系统每行或每列相关的`Dof`。 `Strategy`本身不能找到这些方程，因为每个`Element`可能有不同的自由度，也可能以不同的顺序排列。例如，一个结构元素可以为第一个`Node`定义一个具有所有位移分量的局部系统，然后是第二个`Node`，依此类推。另一个结构元素可以通过首先放置所有`Nodes`的位移的$x$分量，然后是$y$分量和它们的$z$分量来安排其本地系统。因此，一个`Element`的任务是把它的局部系统安排给`Strategy` 。

 `Element`可以给一个`Dofs`的数组，其顺序与本地系统的构造相同，或者得到它们相关的方程ID，并将它们作为一个索引数组给`Strategy`。 `Strategy`使用这些索引将给定的局部系统组装成全局方程组。`Element`的这部分接口由两个方法组成。

\textbf{EquationIdVector} 该方法用于直接给出与局部系统矩阵和向量的每一行或每一列相关的全局方程ID。例如给出`vector`$i=\{24,5,9\}$意味着右手边的第一个元素`vector`必须加到全局系统右手边的第24行，或者本地刚度矩阵的分量$k_{23}$必须加到全局左手边矩阵的分量$K_{59}$。一个`ProcessInfo`被传递给这个方法，以提供这个程序的任何添加参数需求。

\textbf{GetDofList} 这个方法给出了`Element`的`Dofs`，其顺序与本地系统定义相同。 `Strategy`可以使用这个`list`来提取与每个局部位置相关的方程ID，然后用它们来正确组装`Element`的局部系统组件。和前面的方法一样，它需要一个`ProcessInfo`对象作为参数，可以用来传递这个程序所需的任何额外信息。

可以看出，两个方法都以`ProcessInfo`为参数。这个参数似乎是多余的，但在实践中，有些情况下确实需要这样做。例如，在使用分步法[^26]求解流体时，`Element`必须知道哪个是当前的分步，以提供相应的`list`自由度或方程的id。向这些方法传递一个`ProcessInfo`提供这些额外的参数，并保证设计的通用性。

下面是一个为通用结构元素实现EquationIdVector的例子，它可以用于$2 \mathrm{D}$和$3 \mathrm{D}$空间中的不同几何形状。

~~~C++
virtual void EquationIdVector(EquationIdVectorType &rResult,
                              ProcessInfo &rCurrentProcessInfo)
{
    unsigned int number_of_nodes = GetGeometry().size();
    unsigned int dimension = GetGeometry().WorkingSpaceDimension();

    unsigned int number_of_自由度s = number_of_nodes * dimension;
    if (rResult.size() != number_of_自由度s)
        rResult.resize(number_of_自由度s);
    for (int i = 0; i < number_of_nodes; i++)
    {
        unsigned int index = i * dimension;
        rResult[index] =
            GetGeometry()[i].GetDof(DISPLACEMENT_X).EquationId();
        rResult[index + 1] =
            GetGeometry()[i].GetDof(DISPLACEMENT_Y).EquationId();
        if (dim == 3)
            rResult[index + 2] =
                GetGeometry()[i].GetDof(DISPLACEMENT_Z).EquationId();
    }
}

~~~







第三类方法专门用于计算元素变量，主要用于计算后分析结果。一个简单的例子是在获得域内位移后计算结构元素的应力。用户可以要求`Element`使用其内部信息和求解结果来计算附加结果。在这里，一个灵活的接口是非常重要的，可以增加代码的通用性。一个VBI可以用来为这些方法提供一个清晰而灵活的接口。 `Element`开发者可以定义一组的方法来计算与其`Element`相关的变量，用户可以用它们来指定他们想要计算的变量。与计算本地系统的方法类似，为了提高性能和消除创建临时变量所需的冗余时间，结果被作为一个额外的参数传递。为这项任务定义了两组方法。

\textbf{Calculate} 可用于计算元素变量。这些方法被重载，以支持要计算的不同类型的变量。 `Element` 开发者可以覆盖它们来实现计算每个元素变量的必要程序。它们接受用户希望计算的变量作为参数。如果该变量被 `Element` 支持，它将给出结果，如果该变量与此 `Element` 无关，它将什么也不做。结果也作为一个额外的参数被传递，以提高性能并消除创建临时对象所产生的开销。将 `ProcessInfo` 传递给这些方法提供了一种通用的方法来传递额外的计算参数。

\textbf{CalculateOnIntegrationPoints}这组方法不是为整个`Element`计算变量，而是专门在每个积分点计算变量。其界面与之前的方法相同。要计算的变量作为一个参数给出，结果作为另一个参数。 `ProcessInfo`提供计算程序所需的任何额外信息。

提供一个标准的方法来访问`Elements`的邻居，对某些算法来说是非常有用的。问题是，对于其他算法来说，保留`list`的邻居会导致总内存使用的巨大开销。考虑到`Elements`中内存效率的重要性，这些功能被认为是可选的。因此，第一个解决方案是为邻居`Nodes`和邻居`Elements`建立数组，这些数组是空的，在需要的时候填充它们。这种实现方式很好，但空的容器仍然会对简单的`Elements`产生内存开销。在目前的实现中，这些容器被省略了，邻居`Nodes`和`Elements`被存储在元素数据容器中。通过这种方式消除了空容器的开销，现有的容器被重新用来保存这些信息。这个解决方案可用于任何其他必须提供的可选功能，但对其他 `Elements` 没有任何开销。

\subsection{Conditions} 

 `Condition` 被定义为表示应用于边界或域本身的条件。在许多代码中，条件或特别是边界条件是由一个元素表示的，该元素有一个为边界条件修改的公式。在 Kratos 和 `Conditions` 中的设计与 `Elements` 非常相似。它们与 `Strategy` 的交互方式与 `Elements` 相同。 `Strategy`询问他们的本地系统组件，也询问组装过程的信息。使用不同类型而不是`Element`本身的原因是为了澄清这两个对象的不同目的。在通常的有限元模型中，`Elements`比`Conditions`多得多。由于这个原因，一些被认为对`Elements`来说性能太过昂贵或消耗内存的功能可以用于`Conditions`。使 `Element` 和 `Condition` 成为两个独立的类型，可以在不影响 `Element` 的情况下为 `Condition` 增加额外的功能。

\subsubsection{Condition's Requirements} 

 `Condition` 像一个 `Element` 被用来引入新的配方到 Kratos 。因此，添加一个新的`Condition`必须是一个简单的任务，并且不需要对代码进行很大的修改。为实现这一目标，需要将所有计算局部矩阵和结果所需的数据和程序封装在一个对象中，并使用一个清晰的接口。

一个复杂的模型在其边界或域中通常有不同类型的`Conditions`。这就要求`Conditions`相互兼容，以便在一个复杂的系统中一起组装和求解。另一个设计点是让用户改变`Conditions`或在模型中混合它们而没有问题。

像`Element`一样，`Condition`必须有一个非常灵活的界面，因为有各种各样的算法和它们的不同要求。例如，大多数`Conditions`应用于系统的右侧组件，但在某些情况下，如热辐射，它们也影响方程系统的左侧矩阵。由于这个原因，只为右手边的组件创建接口，导致在添加一些`Conditions`时受到严重限制。 `Conditions`不仅在局部组件上不同，而且它们在计算时也需要不同的数据。因此，为了保证实现不同 `Conditions` 的灵活性，必须有一个通用接口。

 `Condition`必须易于实现。有限元开发人员通常不太熟悉高级编程语言的功能，他们喜欢更专注于他们的有限元开发任务。由于这个原因，`Condition`的实现不能过度使用模板或其他编程语言的困难概念。我们的想法是为`Condition`提供一个清晰和简单的结构，使有限元开发人员能够轻松地填写。

对于 `Condition` 来说，性能很重要，但不像 `Element` 那么关键。它的性能很重要，因为它通常在全局程序的内部循环中被调用。因此，`Condition`的性能中的任何小故障都会导致程序执行时间的巨大开销。然而它的重要性不如`Element`的性能，因为模型中`Conditions`较少，全局开销也较小。所以在设计`Condition`时，目的是为了避免产生性能瓶颈的功能。内存效率是另一个需要记住的设计点。如前所述，`Elements`必须避免任何多余的内存使用，因为它们在一个模型中的数量很大。一个模型中`Conditions`的数量通常远远少于`Elements`的数量。这使得 `Conditions` 可以提供被认为对 `Elements` 太昂贵的功能。但是 `Condition` 滥用内存也会产生大量的内存使用开销，必须避免。

\subsubsection{Designing Condition} 

 `Condition` 与 `Element` 非常相似，因此使用了相同的方法来设计它。 `Condition` 被定义为一个持有其数据并计算其本地矩阵和向量以进行组装的对象，也可用于分析后计算本地结果。以这种方式定义`Condition`，将其与代码的其他部分隔离，有助于实现其封装。

像`Elements`一样，`Conditions`必须被设计成可以独立实现，并且可以方便地添加到Kratos中，以保证Kratos的可扩展性。同时，它们必须相互兼容，以便让用户在一个复杂的模型中把它们混合在一起。这里重用了用于`Element`的相同策略模式。图[8.4]显示了应用该策略模式的`Condition`的结构。





在这个结构中，每个`Condition`都分别封装了一种算法，并使它们可以按照我们的要求互换。由`Condition`基类建立的接口也使其派生类相互兼容，并使用户能够将它们混合在一起，为一个多学科问题建模。

 `Condition`与几何学有密切的关系。正如对`Element`的解释，从几何学派生出`Condition`，可以提高几何学派的数据访问性能，但需要为应用于不同几何学派的公式实现不同的`Conditions`，也防止`Condition`与`Elements`或其他`Conditions` 共享一个几何学派。

这种结构降低了几何体的灵活性，也产生了不必要的执行开销。所以这种结构被认为是不合适的，因为对于`Condition`来说，灵活性比性能的小幅提高更重要。

另一种设计是使用`bridge`模式。在我们的设计中引入这种模式，会产生如图[8.5] 所示的结构。

这种模式允许每个`Condition`改变其几何形状，并省略了以前设计的强关系。这样一来，就需要更少的实现。同时，有一个指向几何体的指针允许`Condition`与其他`Conditions`甚至与`Elements`分享它的几何体而没有问题。唯一的缺点是内存中的指针重定向带来的时间开销。





有一个指向几何体的指针，并从它派生出一个访问几何体数据的开销。在设计`Condition`的结构时，还有其他的选择，具有足够的灵活性和更好的性能，但需要在`Condition`中引入编程语言的高级功能，这在我们的设计中是不可接受的。

`Condition`的界面与为`Element`设计的界面相似。这里同样有三类方法。

\textbf{Calculate Local System} 第一组方法是需要计算局部矩阵和向量的。

\textbf{Assembling Information} 这些方法给出了关于本地系统的每一行和每一列在全球系统中的位置的信息。这些信息来自于自由度，`Condition`通过给出其自由度或只是其方程ID来提供这些信息。

\textbf{Calculate} 用于计算与此相关的任何变量`Condition`，通常是取决于条件中的梯度的结果。

正如之前在设计`Element`接口时描述的那样，将计算参数传递给`Condition`方法并将结果作为其返回值，会大大降低性能。为了避免这个问题，结果变量也被传递给每个方法。通过引用传递结果`vector`或矩阵可以防止程序产生暂存器，并提高性能。同时，控制一个给定的结果矩阵或`vector`的大小，并在必要时调整它们的大小，可以优化代码性能。 `Condition`使用与`Element`相同的`set`的方法来计算本地系统的矩阵和向量。 `ProcessInfo`用于传递任何在`Condition`中计算局部系统所需的参数。所有的方法都是为处理密集矩阵而定义的，处理对称或其他类型的矩阵的策略必须使用密集矩阵来与 `Condition` 通信。接口的这一部分由以下方法定义。

 `CalculateLocalSystem` 该方法计算所有本地系统组件。它需要一个左手边的矩阵和一个右手边的 `vector` 来存储结果，以及一个提供分析参数的 `ProcessInfo` 。

 `CalculateLeftHandSide` 这个方法只计算左手边的矩阵。它需要一个储存结果的矩阵和一个提供分析参数的`ProcessInfo`。

 `CalculateRightHandSide` 计算本地系统的右侧分量。它需要一个`vector`来存储结果和一个`ProcessInfo`来提供分析参数。

方法的第二组为`Strategy`提供集合信息，该信息是每个局部系统的行和列在全局方程系统中的对应位置。 `Condition`可以给出一个`Dofs`的数组，其顺序与本地系统的构造相同，或者得到它们相关的方程ID，并将其作为一个索引数组交给策略。 `Strategy`使用这些索引将局部系统组装成全局方程系统。`Condition`的这部分接口由两个方法组成。

EquationIdVector 该方法用于直接给出与局部系统矩阵和向量的每一行或每一列相关的全局方程ID。一个`ProcessInfo`被传递给这个方法，为这个过程提供任何额外的参数需求。

GetDofList 这个方法按本地系统定义的相同顺序给出`Condition`的`Dofs`。 `Strategy`可以使用这个`list`来提取与每个局部位置相关的方程ID，然后用它们来正确组装`Condition`的局部系统组件。像前面的方法一样，它需要一个`ProcessInfo`对象作为其参数，可以用来传递这个过程所需要的任何额外信息。

像 `Element` 一样，`Condition` 使用其数据容器来存储对其邻居 `Nodes` 、 `Elements` 或 `Conditions` 的引用。这个解决方案也可以扩展到存储对最近的`Element`或`Condition`的引用，在联系问题或其他类似信息。

\subsection{Processes} 

创建一个有限元应用程序包括为解决不同的问题实现几种算法。在实践中，每组问题都有自己的求解算法。例如，一个稳态分析算法和一个瞬态算法是不一样的。一个单域过程也和一个多域过程不同，等等。虽然这些算法是代码的核心，而且代码的灵活性和力量取决于它们，但以通用方式处理它们的良好设计变得非常重要。

在有限元代码中处理算法的一个可能的方法是提供一些高水平的类来处理代码中的不同任务 [^33] 。在 Kratos 中，`Process` 类及其派生类被定义为实现不同的算法和处理不同的任务。不同的进程可以用来处理一个非常小的任务，如设置一个节点值到一些复杂的任务，如解决一个流体结构相互作用问题。将一些进程分组到一个较大的进程中也是有帮助的，特别是为了处理复杂的算法而将小的进程组合起来。

\subsubsection{Designing Process} 

 `Process` 可以被视为一个函数类。 `Process`就像一个函数被调用一样被创建和执行。策略模式被用来设计进程的家族。图[8.6]显示了应用于`Process`结构的这种模式。





应用这种模式可以让`Process`独立地封装一个算法，同时提供一个标准的接口，使它们可以相互替换。将每个算法封装在一个`Process`中，而不需要修改代码的其他部分，这使得添加一个新的`Process`非常容易，并增加了库对新算法的扩展性。进程之间的兼容性有助于定制程序流程，在用户想要交换某些算法的情况下非常有用。

`Process`提供的另一个特点是能够将不同的进程结合在一起，并像普通进程一样使用所产生的`Process`。复合模式可以用来实现这一要求。将这种模式应用于 `Process` 的结果是图[8.7] 中所示的扩展结构。





这种结构允许用户将不同的流程合并在一起，并像普通流程一样使用它。在实践中，这种结构对于我们的目的来说被认为是太复杂了。复合模式提供了一个接口来改变每个复合对象的子代。为了简化新流程的实现和结构的总体实现，改变子流程的接口已被删除，`CompositeProcess`必须在创建时获得其所有子流程及其他参数。然而，这个接口可以在将来添加。图[8.8]显示了还原结构。





流程接口相对简单。 `Execute`方法被用来执行`Process`算法。虽然这个方法的参数可以从一个`Process`到另一个`Process`非常不同，但没有办法创建足够的重写版本。出于这个原因，这个方法不需要参数，所有的`Process`参数必须在构造时传递。原因是每个构造函数可以接受不同的`set`参数，而不依赖其他进程或基`Process`类。

\subsection{Solving Strategies} 

在设计完`Process`及其派生类后，我们将关注一个重要的进程系列，它专门用于管理程序中的解题任务。

`SolvingStrategy`是为实现不同求解阶段的 "调用顺序 "而要求的对象。所有的系统矩阵和向量都将存储在策略中，它允许处理多个LHS和RHS。这些策略的微不足道的例子是线性策略和牛顿拉斐尔策略。

 `SolvingStrategy`是由`Process`派生出来的，并使用如图[8.9]所示的相同结构。从`Process`派生出`SolvingStrategy`后，用户可以用组合法将它们与其他一些过程结合起来，以创造出更复杂的`Process`。这个结构中使用的策略模式让用户可以实现一个新的 `Strategy` 并将其添加到 Kratos 中，这增加了 Kratos 的可扩展性。还可以让他们选择一个策略，用它来代替另一个策略，以改变求解算法，这增加了 Kratos 的灵活性。

 `Composite` 模式被用来让用户将不同的策略结合在一起。例如，一个小数步骤的策略可以通过将每一步骤使用的不同策略结合在一个复合策略中来实现。就像 `Process` 一样，改变复合策略的子代的接口被认为过于复杂，因此从 `Strategy` 中删除。因此，一个复合结构可以通过在构建时给出其所有组件来构建，然后可以使用它，但不需要改变其子算法。

`SolvingStrategy`的界面反映了通常有限元算法的一般步骤，如预测、求解、收敛控制和计算结果。这种设计的结果是以下的界面。

\textbf{Predict} 一个预测解的方法。如果它没有被调用，就会使用一个微不足道的预测器，并假定感兴趣的解步骤的值等于旧值。

\textbf{Solve} 这个方法实现了求解过程。这意味着通过组装局部组件建立方程组，使用给定的线性求解器进行求解并更新结果。

\textbf{IsConverged} 它是一种解后收敛检查。例如，它可以用在耦合问题中，以查看解决方案是否收敛。

\textbf{Calculate0utputData} 计算非微不足道的结果，如结构分析中的应力。

策略有时彼此非常不同，但通常全局算法是相同的，只有一些局部步骤是不同的。模板方法模式有助于以一种更可重用的形式实现这些情况。如前所述，该模式单独定义了算法的骨架，并将一些步骤推迟到子类。通过这种方式，模板方法模式允许子类在不改变算法结构的情况下重新定义算法的某些步骤。将这种模式应用于`SolvingStrategy`的结果是图[8.10]中所示的结构。

当算法完全没有变化时，这种结构是合适的，但在我们的案例中，算法从一类策略到另一类策略都有变化。由于这个原因，为了减少算法及其步骤的依赖性，`bridge`模式的修改形式被应用于这个结构。解决模板方法的不同步骤被推迟到另外两个对象上，这两个对象不是从`Strategy`派生的：`BuilderAndSolver`和`Scheme` 。图[8.11]显示了这个结构。

使用这两个额外的`set`对象的主要想法是为了增加代码的可重用性，防止用户从头开始实现一个新的`Strategy`。在实践中，这种结构可以支持有限元方法学中的通常情况，但高级开发人员仍然必须配置自己的`Strategy`，而不使用`BuilderAndSolver`或Scheme。由于这个原因，在目前的结构中，两种方法都可以用来实现求解算法。

\subsubsection{BuilderAndSolver} 

`BuilderAndSolver`是为了执行所有的构建操作和对所产生的线性方程组进行反演而要求的对象。将求解和构建步骤组合在一起的选择不一定是单一的。作出这种选择是为了允许将来代码的并行化，它应该同时涉及线性系统的求解和构建阶段。

由于其特点，`BuilderAndSolver`涵盖了整个求解过程中计算量最大的阶段。这显然需要进行低级别的调整，以确保高性能。一个典型的用户不需要了解这个类的实现细节。然而，对这个对象的作用的理解是必要的。

 `BuilderAndSolver`需要一个线性求解器来解决其构造的方程系统。为了给任何`BuilderAndSolver`分配任何线性求解器的可能性，一个`bridge`模式被用来连接这两组类。这样，`BuilderAndSolver`可以使用任何可用的线性求解器。

`BuilderAndSolver`的接口提供了完整的`set`的方法来分别建立全局方程组或其组成部分。它还提供了建立系统并求解的方法，或者只重建左手边或右手边并求解更新的方程组。该接口由以下方法组成。

\textbf{BuildLHS} 计算全局方程组的左手边矩阵。

\textbf{BuildLHS_CompleteOnFreeRows} 建立与所有自由方向相关的矩形矩阵，同时增加与固定方向相关的列。

\textbf{BuildLHS_Complete} 提供完整的左手边矩阵，而不考虑固定自由度。

\textbf{BuildRHS} 计算并给出全局方程组的右手边`vector`。这种方法对于左手边矩阵在不同求解步骤中是相同的，但右手边在变化的情况下很有用。

\textbf{Build} 构建整个方程组。这种方法提供了同时计算两边的可能性，避免了单独计算每个部分时必须进行的重复计算。

\textbf{ApplyDirichletConditions} 在某些策略中，例如对于标准线性解，`Dirichlet`条件可以通过对`Dirichlet`方程组分区的某些操作有效地应用。这可以通过这种方法来实现。

\textbf{SystemSolve} 使用线性求解器来解决准备好的方程组。

\textbf{BuildAndSolve} 对于大多数算法，调用此方法相当于调用Build，然后调用SystemSolve。它也可以用来实现在解系统的同时建立系统的算法，如前进前沿解法。

\textbf{BuildRHSAndSolve} 这个方法对于更新右边和解决方程组很有用。

\textbf{CalculateReactions} 计算固定自由度下的反应。

还有几种方法用于初始化内部系统矩阵和向量，也可以在必要时从内存中删除它们。 `Strategy` 可以使用这个接口，用上面定义的任何程序实现其算法。

\subsubsection{Scheme} 

 `Scheme` 被设计为 `Strategy` 的可配置部分。它封装了在集合前对本地系统组件的所有操作，以及在求解后对结果的更新。这个定义与时间积分方案兼容，所以`Scheme`可以用来封装Newmark方案。顺便说一下，这个定义更加通用，可以用来封装其他类似的解算组件的操作。

根据模板方法的模式，通常有限元策略中的重要解算步骤被用来设计方案的接口。通常一个有限元求解策略包括几个步骤，如：初始化、初始化和最终确定求解步骤、初始化和最终确定非线性迭代、预测、更新和计算输出数据。考虑到前面提到的步骤，Scheme的界面设计如下:

\textbf{Initialize} 这个方法用于初始化 `Scheme` 。这个方法打算在`Strategy`初始化时只调用一次。

\textbf{InitializeElements} 用于在`Strategy`初始化时调用其Initialize方法来初始化`Element`。

\textbf{InitializeSolutionStep} `Strategy` 在每个求解步骤开始时调用此方法。这个方法可以用来管理那些在时间步长中是恒定的变量。例如，取决于实际时间步长的时间计划常数。

\textbf{FinalizeSolutionStep} 在求解步骤结束时由`Strategy`调用此方法。

\textbf{InitializeNonLinIteration} 它被设计为在每次非线性迭代开始时调用。

\textbf{FinalizeNonLinIteration} 该方法在每次非线性迭代结束时被调用。

\textbf{Predict} 执行对解决方案的预测。

\textbf{Update} 更新数据结构中的结果值。

\textbf{Calculate0utputData} 该方法计算非琐碎的结果。

\subsection{Elemental Expressions} 

有限元方法通常包括首先将治理微分方程转换为其弱形式，然后在适当的近似空间上对其进行离散化，最后推导出作为元素贡献的矩阵形式。Zimmermann和Eyheramendy [^107][^39][^40][^38] 开发了一个从变分形式到矩阵形式的自动符号推导环境，并将其与建模工具集成到一个统一的环境中 [^105] 。现在有几个计算机代数系统，如Matlab [^68] , `Mathematica` [^103] 和Maple [^66] 可以做这种类型的符号推演。在Kratos中，将变分方程改为弱形式的第一部分专门用于以前的工具，只有`set`的工具被设计和实现，以帮助用户将其弱形式转换为矩阵形式的元素贡献。

元素表达式的设计和实现是为了帮助用户在 `Element` 中编写他们的弱形式表达式。主要的想法是创建一个 `set` 类和重载运算符来理解弱形式的表述，并根据它来计算局部矩阵和向量。

例如，在一个简单的热传导问题中，主控方程是:

\begin{equation}
-\nabla^{T} \mathbf{k} \nabla T+Q=0
\end{equation}

其中$T$是领域内的温度，$Q$是领域内的热源。将此方程转换为其弱形式，可得到以下方程 [^104] 。

\begin{equation}
\mathrm{ST}+\mathrm{f}=0
\end{equation}

其中元素矩阵$\mathbf{S}$为。

\begin{equation}
S_{i j}=\int_{\Omega}\left(\nabla N_{i}\right)^{T} \mathbf{k} \nabla N_{j} d \Omega
\end{equation}

和元素的右边`vector` $\mathbf{f}$定义如下:

\begin{equation}
f_{i}=\int_{\Omega} N_{i} Q d \Omega+\int_{\Gamma_{q}} N_{i} \bar{q} d \Gamma
\end{equation}

对于各向同性的材料，电导率$k$可以从积分中提取，得到的方程为：。

\begin{equation}
S_{i j}=k \int_{\Omega}\left(\nabla N_{i}\right)^{T} \mathbf{I} \nabla N_{j} d \Omega
\end{equation}

或者。

\begin{equation}
S_{i j}=k\left(\nabla_{i} N_{l}, \nabla_{j} N_{l}\right)
\end{equation}

这个方程可以通过以下代码在`Element`中实现。

~~~C++
for (int i = 0; i < nodes_number; i++)
    for (int j = 0; j < nodes_number; j++)
        for (int l = 0; l < integration_points_number; l++)
        {
            Matrix const &g_n = shape_functions_gradients[l];
            for (int d = 0; d < dimension; d++)
                rLeftHandSideMatrix(i, j) += k * g_n(i, d) * g_n(j, d) * w_dj;
        }
~~~



使用元素表达式，同样的表述可以用更简单的形式写成：。

~~~C++
KRATOS_ELEMENTAL_GRAD_N(i, l)
grad_Nil(expression_data);
KRATOS_ELEMENTAL_GRAD_N(j, l)
grad_Njl(expression_data);

noalias(rLeftHandSideMatrix) = k * (grad_Nil, grad_Njl) * w_dj;

~~~


可以看出，后面的形式符合方程的符号符号，这使得它更容易实现。$\mathrm{C}++$中提供的重载运算符是实现理解这种符号所需代码的起点，但简单的重载结果，由于创建了多余的临时对象，性能不佳。3.4.2节中描述的表达式模板技术可以用来将上述表达式自动转换为以前的手写形式。第3.4.2节中描述的模板元编程也被用来强加张力符号。所有这些技术都被用来评估符号记号，并为每种情况生成专门的代码。下面是`vector`"，"重载运算符的例子。


~~~C++
template <unsigned int TIndex1,
          unsigned int TIndex2,
          class TExpression1,
          class TExpression2,
          class TVectorType1,
          class TVectorType2>
typename result_type
operator,
    (Elemental1DExpression<TIndex1,
                           TExpression1,
                           TVectorType1> const &rVector1,
     Elemental1DExpression<TIndex2,
                           TExpression2,
                           TVectorType2> const &rVector2)
{
    return outer_prod(rVector1()(), rVector2()());
}
template <unsigned int TIndex1,
          class TExpression1,
          class TExpression2,
          class TVectorType1,
          class TVectorType2>
double
operator,
    (Elemental1DExpression<TIndex1,
                           TExpression1,
                           TVectorType1> const &rVector1,
     Elemental1DExpression<TIndex1,
                           TExpression2,
                           TVectorType2> const &rVector2)
{
    return inner_prod(rVector1()(), rVector2()());
}

~~~




第一个重载版本将用于两个向量具有不同索引的情况，并实现这两个向量的外积。而第二个版本将用于两个给定的表达式具有相同的索引并实现这两个表达式的内积 `vector` 。值得一提的是，第一个版本返回表达式，而不是计算出的矩阵，并且使用表达式模板技术来优化其效率。

有类似的操作符来处理不同情况的矩阵操作，这取决于它们的索引。同时，为了进一步简化元素表达式，还增加了跨域积分的功能。

在当前版本的Kratos中，元素表达式仍处于实验阶段。然而，一些基准测试表明，它们的效率可以与手工编码的`Elements`相媲美，因为它应该是。

\subsection{Formulations} 

 Kratos被设计用来支持有限元方法中的元素方法。对于某些问题，元素法的结果不如其他方法，如节点公式。Formulation被定义为实现所有这些方法的地方。Formulation还没有实现，但被认为是 Kratos 的未来功能之一。



\section{Input Output} 

一般来说，大多数的有限元应用程序必须与前处理器和后处理器进行通信，除了在一些特殊情况下，应用程序产生自己的输入。这使得输入输出（ IO ）成为应用程序的重要组成部分。

在这一章中，首先讨论了设计应用程序IO的不同方法，并介绍了一个灵活和通用的IO结构。接下来是专门写解释器的部分，其中包括对概念的小范围介绍，以及对相关工具和库的使用的简要解释。接下来描述了使用Python作为解释器的情况，并解释了使用Python的原因。最后简要介绍了使用boost python库的情况。

\subsection{Why an IO Module is Needed?} 

一个典型的有限元程序包括从输入源获取数据，对其进行分析并将结果发送到输出。有一些应用程序使用嵌入式IO结构。这意味着它们的IO程序在需要IO操作的子程序中实现。例如，元素在需要时直接从输入文件读取其属性，等等。

这种方法相当简单，容易实现，在某些情况下可以消除一些数据存储的开销。然而，在某些情况下，嵌入式IO方法会给实现带来一些困难或限制了程序的灵活性。

在复杂的问题中，以前的简单方案变成了重复或多输入输出方案。这些策略的变化可能会引入新的IO语句，改变其中一些语句或使一些现有的语句失效。这可能导致在代码的不同部分出现许多IO方法，这些方法基本上做同样的事情，但目标不同。创建一个IO模块并在所有这些语句中使用它，有助于我们统一这些工作，简化代码的维护。

对于一个有限元程序来说，连接不同程序的可能性是一个很大的附加价值。使用不同的前处理器和后处理器或者连接不同的有限元程序是一些典型的例子。连接到每个程序意味着以给定的格式读取其输出（在前处理器的情况下），并以其可识别的格式生成其输入（在后处理器的情况下）。很明显，每个程序都可能有与其他程序不兼容的格式。使用嵌入式IO方法会导致程序非常僵化，难以扩展到任何新的IO类型。在这种方法中，需要对代码进行全面修订以增加任何新的接口，所有的IO语句必须被修改以包括新的格式。在中间放置一个IO模块可以解决这个问题。任何来自内部或外部的请求都要通过IO，它负责将外部格式翻译成内部格式，反之亦然。现在任何新的连接都可以通过一个新的IO对象添加。





创建一个IO模块可以减轻团队开发应用程序的难度。如果没有它，每个开发人员可能会在自己的部分放置一个IO声明，并导致与其他人在使用文件时发生冲突。在同样的概念中，方便的文件管理是使用IO模块的另一个原因。 IO模块可以打开一次文件，对其进行处理，也可以在工作结束后关闭它。如果文件打开时有问题，它可以报告并重新尝试。最后，以这种方式处理多个文件也被简化。

作为结论，有一个单独的、强大的IO模块来处理程序的输入和输出任务是非常有用的，可以毫无问题地处理所有不同格式的方案。通过这种方式，可以实现灵活性的保证和可扩展性。

\subsection{IO Features} 

在开始设计IO之前，重要的是对不同方案的IO的不同方面和特点进行分类。这有助于收集一个通用的多学科项目所需的IO特征。

\subsubsection{IO Medium Type} 

在有限元分析领域工作的人习惯于将文件作为输入和输出媒介。这在许多情况下是正确的，但并不总是如此。一般来说，IO介质可以是文件、一些控制台流、网络通信的套接字或任何其他介质。有时媒介的差异来自于平台和操作系统。例如，在Linux中打开一个文件和写入文件可能与Windows不同。这使得它们实际上，更多的是从实现的角度来看，是不同的媒体来交互。

对IO的这种观点创造了一些在设计阶段之前需要回答的开放问题。IO是只想与一种类型的媒介互动还是更多？它是否应该在未来与新的媒体互动方面具有可扩展性。

我们的设计可以取决于这些问题。只与一种类型的媒体合作，可以简化IO的界面，减少对多种媒体支持的实施工作。这种差异可大可小，取决于设计和实现，也取决于支持的媒体类型和它们的不同方式。让我们举个例子，考虑把控制台IO作为第一个交互的媒介。这个媒介的接口只需要一个读取和打印方法来进行通信。这里假设控制台流总是可用的。图[9.1]显示了这个简单的接口。

现在让我们添加一个文件作为一个新的媒介。这里提供了读取和`Write`方法用于文件访问。不幸的是，文件并不总是可以像控制台那样被读取或写入。我们需要在任何访问之前打开它，并在程序结束时关闭它。文件的这种性质 IO 为我们的接口引入了两个新的方法： `Open` 和 `Close` 。图[9.2] 显示了文件 IO 的界面。





现在，为了拥有一个多媒体IO，我们提供了一个给定接口的联合体，以便与两个媒体顺利互动。





很明显，有了文件接口，并统一了打印和 `Write` 方法，就不需要额外的方法来处理控制台 IO 。这个例子的意图是显示每种媒介的不同性质，而不是一个严重的设计问题。

处理每一种新的媒介可能会在接口中引入一些新的方法，而要使其可扩展，就需要在其中加入新的一层。换句话说，要做到这一点，需要另一层封装来分离不同的IO模块，同时保持所有模块的既定接口。现在，让我们重新设计我们之前的例子，看看如何组织它，使其具有可扩展性。图[9.4]显示了新的结构。

有了上述结构，任何新的媒介都可以通过图[9.5]所示的新封装层使用IO接口来支持。 









\subsubsection{Single or Multi IO} 

一个简单的有限元程序包括从输入源获取数据，对其进行分析并将结果发送到输出介质，如图[9.6]所示。静态结构分析程序就是一个很好的单例 IO 。

这种简单的方案也可以应用于一些更复杂的问题，在这些问题中，对程序流程的某些点有各种IO语句。瞬态热问题、流体动力学和结构动力学是这些类型解决方案的典型例子。在所有这些问题中，程序首先读取模型数据，然后开始分析，同时写出每个时间步骤的结果。从设计的角度来看，这个方案与之前的方案类似，因为IO语句总是相同的。在读取或写入点时，没有因算法而发生变化。

在高级问题中，有些情况下IO语句会因算法的性质而改变。换句话说，算法会根据问题的状态来读取数据或写入数据。例如，寻找一些收敛标准或位移规范，而不仅仅是在某些特定的点，如分析的开始或时间步骤的结束。优化和交互算法就属于这个类别。这种情况意味着在设计的时候对IO进行更多的封装。原因是需要额外的灵活性来处理这些情况，这导致IO更加独立。











\subsubsection{Data Types and Concepts} 

IO的主要任务是传输数据。一个有限元程序通常与不同类型的数据一起工作，如`double` 、`vector` 、矩阵等。同时，每个数据代表一个概念，如位移、温度、节点的ID等等。在数据类型和要支持的概念方面，又要做出决定。哪些数据类型需要被IO所支持？它是否需要对新的类型进行扩展？它是否支持新的概念？

这些决定都与格式有关。它们也深深地影响着 IO 的实现。从设计的角度来看，接口也受到这些决定的影响。现在，让我们介绍一些例子来解释这些关系。

一个简单的热力应用就是一个很好的刚性例子 IO 。它必须从输入中读取整数、双数、向量和矩阵等概念，如节点信息、元素信息、热导率和温度等属性以及热通量等条件和初始值。输出处理写温度和通量所需的双数和向量。所以在这种情况下，所有的类型都是已知的，所有的概念也都被定义，如[表9.1]所示。

必须提到的是，人们可以只处理双数，并将上述所有类型的组成部分视为双数，从而进一步简化 IO 。对于输入部分，基于列的格式是很适合的，也使实现非常简单。

~~~C++
# Format: NodeId X Y Z Temperature IsFixed
1 0.00 2.00 0.00 0.00 0
2 1.00 1.00 0.00 1.00 0
3 2.00 4.00 0.00 0.00 0
4 3.00 5.00 0.00 0.00 0
5 4.00 0.00 0.00 2.50 1
......

~~~



在这个例子中，IO的实现可以通过正常的流媒体或逐行扫描轻松而高效地完成。最后，当所有的类型和概念都是已知的时候，接口可以被非常简单地定义（虽然是非常僵硬的）。

~~~C++
Read(NodesArray Nodes , ElementsArray Elements)

Write(vector Temperature , matrix ThermalFlows)
~~~

可以设计一个更复杂的界面，以便对IO序列提供更多的控制。 例如

~~~C++
Read(NodesArray Nodes , ElementsArray Elements)

ReadNodes(NodesArray Nodes)

ReadElements(NodesArray Nodes , ElementsArray Elements)

Write(vector Temperature , matrix ThermalFlows)

WriteTemperature(vector Temperature)

WriteThermalFlow(matrix ThermalFlows)

~~~


下一个例子是一个拉普拉斯领域的应用。这指的是任何由拉普拉斯方程支配的问题，如热、势流、低频电磁、渗流问题或它们的任何组合，并解决它。在这个例子中，概念从一个问题到另一个问题，或有时从一个元素到另一个元素都在变化，但输入的类型总是相同的。与之前的例子相比，输入格式变得更加复杂，因为概念定义必须被添加到其中,这里的标签机制有助于区分要读取的不同概念。

~~~C++
# Format: NodeId X Y Z initialvalues IsFixed
1 0.00 2.00 0.00 VELOCITY_X 0.00 0
2 1.00 1.00 0.00 VELOCITY_X 1.00 1
3 2.00 4.00 0.00 TEMPERATURE 0.00 0
4 3.00 5.00 0.00 TEMPERATURE 0.00 0
5 4.00 0.00 0.00 TEMPERATURE 2.50 1
......

~~~



概念的灵活性在实现中引入了一个额外的成本。需要一个查找表来处理通过给定的名称读取不同的变量，并在内部对其进行分配。这里要使用的查询表可以是一个简单的，每个变量的名称和它们的唯一索引，如[表9.2] 所示。



由于这些新功能，界面设计也必须改变。 首先，需要一个初始化方法来传递查找表和设置 IO 。然后`Write`方法必须被修改，以获得要写入的变量作为其参数。这是必要的，因为在这个例子中，变量并不总是`Temperature`，它的`Flow`和方法的名称不能依赖于它。

~~~C++
Initialize(Table LookupTable)

Read(NodesArray Nodes , ElementsArray Elements)

Write(string VariableName , double Value)

Write(string VariableName , vector Value)

Write(string VariableName , matrix Value)
~~~


或者更完整的版本:
~~~C++
Initialize(Table LookupTable)

Read(NodesArray Nodes , ElementsArray Elements)

ReadNodes(NodesArray Nodes)

ReadElements(NodesArray Nodes , ElementsArray Elements)

Write(string VariableName , double Value)

Write(string VariableName , vector Value)

Write(string VariableName , matrix Value)

~~~




可以看出，在这个接口中，`Write`对于输出处理的所有不同类型都是重载的。

最后一个例子是一个通用IO模块，它的概念和类型都可以扩展到新的。在这种情况下，类型信息和一些基本操作必须包括在查找表中。这使情况更加复杂。为了看到这一点，让我们在前面的例子中增加一个类型的可扩展性，并使用它来引入一个复数到 IO 。

 IO 处理新类型需要知道如何对它们进行一些基本操作。例如，如何将字符串转换为它们，反之亦然，以及如何创建它们。因此，对于每个新的类型，有必要给它一些辅助函数。这些函数将帮助IO操作新类型。这些函数使IO免于知道关于新类型的任何事情。我们可以将所有这些帮助函数归入一个TypeHandler类，并将其传递给 IO 。图[9.9] 显示了这个结构。





以前的接口也会因为这个新机制而改变。例如，写一个接口可以改成这种新的形式。

~~~C++
Write(string VariableName, void *Value,
      TypeHandler &ThisTypeHandler)
~~~

现在，`Write`方法不知道变量的类型，只是得到一个空指针，然后通过给定的类型处理程序来处理。这个解决方案可以工作，但它不是类型安全的，并留下了发生严重错误的可能性。一个例子是传递一个整数值和一个矩阵类型处理程序。这使得`Write`方法写入垃圾，而Read方法可能会因侵犯内存而导致系统崩溃。有一个更优雅的方法来处理这个问题，那就是使用模板。在这种方式下，不需要将数据转换为空指针，而且给定数据的类型将在编译时被检查。


~~~C++
template <class TTypeHandler>
void Write(string VariableName,
           typename TTypeHandler ::DataType &Value,
           TTypeHandler const &ThisTypeHandler)

~~~


再往前走一步，这个TypeHandlers可以被添加到查找表中，使这个机制更加自动化。通过这种方式，IO对每个概念都像以前一样检查查找表，并获得处理这个概念的所有工具，即使是一个新的信息类型。

值得一提的是，变量基础接口大大有助于克服所有这些复杂的问题，并设计一个保持清晰和灵活的通用接口，这一点在后面的章节中解释。

\subsubsection{Text and Binary Format} 

文本文件是一个以文件形式存储的字符序列，取决于它的内容，是人类可读的。一般来说，这些文件包含ASCII字符，其中ASCII（美国信息交换标准代码）是一种基于英文字母的字符编码。

将字符串写入文本文件很简单，因为不需要转换。但要写一个数字，首先要把它转换为字符串格式，然后才能写到文件中。例如，要写一个存储1234567890的整数变量，首先必须将其在内存中的二进制形式0100 1001 1001 0110 0000 0010 1101 0010 转换为其代表符号 "1234567890"，然后存储到文件中（图[9.10]）。

在再次读取时，字符串被直接读取，但数值必须从字符串中转换（图[9.11]）。

另一方面，二进制格式对每个对象的二进制值进行处理，而不是像文本文件那样对其代表性字符串进行处理。以这种方式，一个字符串或一个数字以同样的方式转储到文件中。例如，对于上述同一个整数变量，只需将二进制值0100 1001 1001 0110 0000 0010 1101 0010直接写入文件即可（图[9.12]）。

同样，在读取时只需读取字节序列并直接放入变量（图[9.13]）。

文本格式的优势在于其可读性。这使得它成为小型和学术应用的一个很好的选择，因为输入数据的方面可以被验证，甚至可以手动改变，而且输出可以在没有任何后处理程序的情况下被检查。例如，查看所有的结果是否为零。这种格式的缺点是它在读或写数字时的延迟时间，因为从其代表字符串转换的时间。另一个缺点是在存储数字数据的数据大小方面有很大的开销。这是因为在大多数情况下，一个数字的代表字符串所占用的内存比该数字还要多。看看我们之前的例子，整数1234567890像其他整数一样在内存中占据4个字节。将其转换为字符串需要10个字节，每个字符一个字节，最后没有null，这是$150 \%$开销。然而，对于小数字，小于4位的数字，代表字符串较小，但一般来说，有限元数据通常由较长的数字数据组成。这两个问题使得在有限元应用中很难使用文本格式处理非常大的问题。

使用二进制格式的第一个优点是可以直接读写数据，这比文本文件格式的性能要高。使用二进制格式的另一个优点是存储的数据没有实际大小的开销。这两个优点使这种格式成为大问题数据存储的良好选择。二进制格式不是人类可读的，使用它作为输入和输出格式会使调试过程复杂化，使用户无法手动检查数据是否有明显的错误，比如所有坐标都等于零等等。最后，由于没有兴趣打开和阅读二进制文件，它们可以被压缩以占据比通常所需更少的空间。

使用Ascii或二进制格式更多影响的是实现细节，而不是其设计。接口和结构可以是相同的，实现细节可以通过IO模块隐藏。因此，使用其中一种不会影响执行成本，选择只取决于哪一种更适合我们的需要。

\subsubsection{Different Format Supporting} 

上一节从格式化的概念开始，描述了两种不同类别的格式，文本和二进制。在本节中，我们从格式支持的角度来看待这个概念。

格式是数据在一个媒介中的表示和组织方式。数据表示方式或其组织的任何变化都会产生一种新的数据格式。正如上一节所述，文本和二进制格式在放置数据的方式上是不同的。以同样的方式，改变每一种数据组织方式都会导致新的格式，无论是二进制还是文本。

如今，大量的数据格式被定义并被不同领域的程序使用。其中一些是流行的，而许多只是针对特定的程序。此外，每种不同的格式都有一些优点和缺点，正如之前看到的文本和二进制格式。所有这些方面使我们有必要在开始实施之前做出一些决定。使用现有的格式还是创建一个新的格式？支持一个额外的格式还是一个就够了？









IO是否可以在未来扩展到支持新的格式？

大多数这些决定对设计和实现的影响与支持媒体的方式相同。第一个选择是只支持一种格式。选择这个选项使设计和实现都非常简单。这种情况下的接口可以非常简单，没有任何参数或方法来指定格式。实现也很简单，因为既没有开关和案例来处理给定的格式，也没有为不同的格式重复的方法。图[9.14]显示了一个只支持文本格式的IO的接口实例。

值得一提的是，上面提到的实现的简单性是指全局设计方面，而不是处理每个特定格式的成本，这可能要花费一个完整的`parser`来实现。

为了利用一种以上的格式，必须修改以前的设计以分别处理每一种格式。接口必须为用户提供某种方式来`set`他们的格式。对于要处理的少量格式，一个简单的方法是创建单独的方法。这导致了一个非常清晰的接口，并在某些情况下简化了实现。这种设计从一个方面保持了每个格式支持的独立实现，因此它增加了代码的可维护性。然而，它使代码更加僵化和静态。图[9.15]显示了这种双格式支持的界面的一个例子。可以看到它的清晰性和可读性，而对于支持更多的格式来说，这种方法显然是不合适的。

有一种更优雅的方法，它只是通过接口传递一个标志来识别正在使用的格式。在这种方式下，界面是清晰的，添加一个新的格式是很容易的，而通过层次结构使用它是没有什么帮助的。图[9.16]显示了前面的例子和一个新的接口。现在的界面很简单，而且比以前更有活力。

现在让我们再进一步，在不改变以前的格式的情况下，灵活地增加新的格式。这可以用之前的方法来实现媒体的可扩展性。在IO结构中发生了一个新的封装层，以统一接口，对每种格式的支持被单独封装。这种设计提供了一个灵活的结构，增加新的格式不会改变代码的任何其他部分，并赋予其可扩展性。这种设计要求所有支持的格式必须使用一个独特的接口。因此，为了完成这个结构，一个通用的接口需要以同样的方式处理所有的格式，没有关键的限制。重复使用以前的例子来应用新的设计，会产生一个更灵活的结构。图[9.17]显示了这个新结构。

现在支持一个新的格式意味着添加另一个部分，而不需要对其他IO部分进行任何修改。图[9.18]显示了扩展版本。

\subsubsection{Data IO and Process IO} 

单一目的有限元应用程序通常从输入中获取数据，对其进行处理并将结果写入输出。这些应用程序总是使用他们自己实现的算法来解决这个问题。他们也给用户一些选项，通过输入数据修改算法的某些方面，但不是算法本身。这些程序的典型输入包含节点、元素、条件、属性和一些算法参数，如收敛标准、最大迭代次数等等。其他一些代码也可以通过一些给定的选项来选择算法的某些部分，如改变求解器、静态或动态策略等。

对于更复杂的问题和多学科的程序来说，前面的方法过于死板，无法适用于所有问题。例如，对于一个热机械应用，有时不是从一开始就想要机械解决方案，而是从某个时间开始。因此，有必要建立一种机制，从某个时间开始求解机械部分，以避免不必要的计算开销。另一个例子是流体结构的相互作用问题，这可能导致程序根据结构和流体相互作用的类型改变其全局策略。所有这些都使得开发者在实现他们的代码时，不仅要从用户那里获得数据，还要获得解决方案所需的过程。

第一种方法是创建一个类似于输入数据的半语言，它可以控制代码块的执行顺序，还可以选择在算法的特定语句中添加流程。通过这种方式，用户可以通过输入文件选择何时读、写、解等来定义程序流程的全局布局。此外，它还可以在某些地方放置一些额外的进程，如在每个时间步骤的开始等。这种方法限制了修改程序算法的灵活性，这来自于两个事实。 首先，在输入部分缺乏一个好的解释器，使得不可能引入整个算法。第二是代码的内部设计，它没有很好的分割，会限制人们把不同的算法放在一起以创建一个新的算法的方式。

通常用fortran编写的代码，由于其编写解释器的设施有限，所以使用以前的方法处理不同的算法。

另一种方法是实现一个高级语言解释器作为输入，以管理程序的全局流程。这给了代码很大的灵活性，新的算法可以很容易地实现。这种方法的一个工作例子是Cardona等人`Elements`开发的面向对象的有限[^77]方法，它有自己的命令解释器来处理多学科问题的不同算法。其优点是输入语言语法的灵活性。人们可以定义一个非常小的语言，很好地采用它的需求。这可以为某些类型的应用提供一个清晰而强大的输入格式。这种方法的缺点是实施成本高。编写一个好的解释器是一项艰巨的工作，而维护它更是难上加难。C++程序员更愿意写自己的解释器，因为有很多可用的库可以帮助他们 [^67][^6][^2][^5] 。即使使用库和编译器创建工具，为有限元程序实现一个解释器也会给它的成本带来很大的开销。另外，许多有限元程序员的背景中没有编译器编写的知识，学习或雇用一些专家又会造成程序的额外成本。

最后一个选择是使用一个已经实现的解释器，而不是编写一个新的解释器。这种方法的第一个优点是省去了开发新解释器的成本和时间开销。另一个好消息是不需要知道如何写一个编译器。另外，现有的语言也有它的文档，如教程、参考手册等等。这就减少了程序IO的文档，只记录了语言的扩展部分和它的具体命令。然而，这种方法也有其缺点。 首先，是将程序连接到解释器的努力可能是巨大的，这取决于FE代码的编程语言和选择的解释器。第二个缺点是流行语言的通用概念，这可能使它们不能很好地适应有限元概念。另一个落后的地方是它们在内存和性能方面的开销。毕竟还是有好消息的。对语言及其解释器的良好选择可以减少或有效消除这些缺点。有许多工业应用使用这种方法，例如ABAQUS [^10][^11] 使用Python [^89][^63] 或ANSYS [^1] 使用TCL/TK。

\subsubsection{Robust Save and Load} 

操作任何种类的文件的程序通常必须提供一些机制来保存和重新加载他们的文件。在计算机科学中，一个在程序执行后保持其状态的对象被称为持久化对象。同时，持久性也被称为程序存储和检索其数据的能力。有限元应用程序通常不需要以读取模型的方式来保存它们。相反，他们只是读取模型并写入结果。前处理程序和后处理程序负责存储模型和处理后的结果供将来使用。但在有些情况下，有限元求解器还是需要保存和加载功能。例如，在分析大的问题时，由于某些资源的安排，有时需要停止进程并在稍后恢复。一个典型的原因是使用夜间可用的计算机工作。这使得代码有必要在结束进程前存储其状态，并在下次执行时从最后的状态中检索出来。

有不同的方法来实现持久化。两个典型的方法是对象持久化和全局或数据库持久化。

对象持久化遵循的理念是，每个对象都知道如何使用某些接口来存储和检索自己。这种方法也被称为序列化。这是因为在这种机制中，数据是以串行方式存储和检索的。为了实现一个序列化机制，首先要为所有的对象引入一个接口，提供一个统一的保存和加载其状态的形式。例如，为每个对象包括一个`Serialize`方法。然后每个对象实现其存储和检索数据的方式。对于由简单数据组成的对象，可以简单地按顺序存储数据并按相同的顺序读回。对于一个由其他对象和一些简单数据组成的复杂对象的保存和加载，包括将简单的数据按原样放入，并按顺序调用所有成员对象的`Serialize`方法，然后再按同样的顺序调用成员`Serialize`方法加载它们。重要的一点是调用每个组件的`Serialize`方法，这将导致每个部分序列化其数据并再次调用其组件的`Serialize`。这样一来，从数据金字塔的顶端开始保存或加载，系统将自动遍历到底部，并覆盖从上到下的所有数据。

这个方法有一个例子。让我们为一个简单的`Mesh`类实现一个序列化机制，该类包含`Nodes`和`Elements`，每个`Element`有其`Properties`，如图[9.19]所示。

第一步是通过给所有对象添加`Serialize`方法来创建必要的接口。图[9.20]显示了这个接口。

现在让我们来实现`Serialize`方法。虽然`Mesh`本身没有任何简单的数据，但它只是调用所有`Nodes`和`Elements`的`Serialize`方法，也有一些额外的信息用于检索自己。


~~~C++

Serialize(File, State)
{
    if (State == IsRead)
    {
        File >> NumberOfNodes;
        File >> NumberOfElements;
        CreateNodesArray(NumberOfNodes);
        CreateElementsArray(NumberOfElements);
    }
    else
    {
        File << NumberOfNodes;
        File << NumberOfElements;
    }
    for (int i = 0; i < NumbreOfNodes; i++)
        NodesArray[i].Serialize(File, State);
    for (int i = 0; i < NumbreOfElements; i++)
        ElementsArray[i].Serialize(File, State);
}

~~~




`Node`的`Serialize`方法非常简单，只是传输其两个简单的数据，`Id`和`Coordinates` 。

~~~C++
{
    if (State == IsRead)
    {
        File >> Id;
        File >> Coordinates[0] >> Coordinates[1] >> Coordinates[2];
    }
    else
    {
        File << Id;
        File << Coordinates[0] << Coordinates[1] << Coordinates[2];
    }
}

~~~

但是， `Element` 拥有像Id和Properties 组分这样的data。操控他们，可以利用Serialize 方法:

~~~C++
Serialize(File, State)
{
    if (State == IsRead)
    {
        File >> Id;
        File >> NodesIds[0] >> NodesIds[1] >> NodesIds[2];
    }
    else
    {
        File << Id;
        File << NodesIds[0] << NodesIds[1] << NodesIds[2];
    }
    Properties.Serialize(File, State);
}

~~~



最后，`Properties`的`Serialize`方法完成了将其数据添加到文件中。


~~~C++
Serialize(File, State)
{
    if (State == IsRead)
    {
        File >> Id;
        File >> Conductivity;
    }
    else
    {
        File << Id;
        File << Conductivity;
    }
}

~~~


现在要保存`Mesh`，我们只需要调用它的`Serialize`方法，并加上一个写入标志。

~~~C++
mesh.Serialize(File , IsWrite );
~~~


这将导致所有`Nodes`和`Elements`在输出文件中写入自己，同时每个`Element`也会导致其`Properties`写入自己。图[9.22]显示了在输出文件中写入数据的顺序。

要从文件中获取数据，只需再次调用`Mesh`的`Serialize`方法，但这次要有读取标志。
~~~C++
mesh.Serialize(File , IsRead );
~~~

这个方法首先获取`Mesh`信息，然后开始加载`Nodes`，之后是`Elements`，如上面的伪代码所示。看一下图[9.22]中显示的写入数据的顺序，可以很容易地验证，保持相同的写入和读取顺序，会产生相同的结构。

在这个简单的例子中，`Mesh`通过知道有`Node`或`Element`创建对象。在实践中，有很多情况下，对象的类型在读取之前是不知道的。例如，一个真实的网格可以有各种类型的元素。那么我们如何在调用 `Serialize` 方法之前创建它们呢？有两种方法来解决这个问题。第一种方法是使用 $\mathrm{C}++$ 运行 `Time Type Identification` ( `RTTI` ) 来存储对象名称，并使用此名称创建对象。第二种方法是手动命名每个对象，并将其作为一个参考名称存储，以便再次创建它。第一种方法非常稳健且易于使用。它适用于任何类型的类，并且非常容易扩展。第二种方法不那么稳健，因为名字是手动给出的，任何巧合都可能导致问题，但它更具有可调性，因为可以给一个类的不同状态赋予不同的名字。这两种方法都需要一个注册数据库，对于任何给定的对象名称，它都会指出创建它的注册方式。

现在让我们来看看指针管理和序列化指针。考虑到一个元素类，它保留了指向其节点的指针而不是它们的索引。应用我们的标准方法将保存存储在指针中的节点的内存地址。虽然恢复后的对象可以在内存的其他位置，但这个存储的地址对于恢复节点是完全无用的。一个更谨慎的实现可以保存一个指向性节点的副本，但这将导致重复的节点，而且这也不是我们想要的结果。解决方案是给每个对象一个唯一的索引，并通过这些索引存储和检索指向的对象。在我们的有限元案例中，节点和元素的索引很适合这个目的。

串行化在很大程度上取决于程序的内部结构。任何时候，对象的一个组成部分发生变化，之前存储的数据就会失效。使用版本和按版本检索数据可以解决这个问题，并有助于在实践中使用它。

一般来说，序列化是非常可扩展的，并且很适合作为保存和加载任务的通用解决方案。有一些库提供了很好的序列化库的实现，可以在其他代码中使用，以最小的代价提供持久性。XParam[^9]和Boost Serialization库[^4]就是这些库的例子。微软基础类（MFC）库也包含了一个序列化机制，但使用该机制会给代码带来平台依赖性，使其不能移植到其他平台。另一方面，数据库的持久性来自于数据库必须知道如何保存和加载其数据的想法。这种方法的可扩展性较差，但它在与数据库合作时效果很好。任何可以存储在给定数据库中的新对象，即使是它的组件，也可以通过现有的IO来存储和恢复，而无需修改。这就是这种方法的最大优势。

\subsubsection{Generic Multi-Disciplinary IO} 

在简单介绍了IO中的每个不同功能后，现在让我们看看哪些是必要的，哪些对通用的多学科IO有用。

\textbf{Supported Media} 支持的媒体可以被认为是一个可选的功能。然而，为了给我们的代码增加可移植性，或者在某些情况下使它成为一个可用的库，它可能成为必要的。像对待格式一样对待媒体可以提供一个有效的解决方案，以保持对新媒体的可扩展性，而不需要投入额外的努力。

\textbf{Single or Multi IO} 很明显，在设计一个通用的IO时，并没有关于它将如何被使用的线索。这就增加了我们设计中的封装量，这也是使其在多IO方案中可用的关键点。另外，许多多学科的算法都在使用多IO方案，这使得它成为我们设计中的一个重要部分。

\textbf{Data Types and Concepts} 可以扩展到新的概念，对于一个通用库和多学科库来说是至关重要的。对新类型的可扩展性对泛型库来说是至关重要的，以保证其使用的便利性。

\textbf{Different Format Supporting}尽管支持不同的格式并不是一个必要的功能，但它对任何通用库来说都是一个巨大的附加值。开发一个具有新格式可扩展接口的通用库可以增加生成的库的使用，因为任何新的用户都可以使它与已有的格式一起工作。所有这些使得格式的可扩展性成为我们IO功能的一部分来实现。

\textbf{Text or Binary Format} 选择一种格式取决于程序的使用，而不是取决于它的多学科性质。决定拥有一个可扩展的多格式 IO 为在必要时实现文本或二进制格式开辟了道路。

\textbf{Data or Process IO} `Process IO` 是多学科的基本特征之一。 代码不断变化的来源，没有过程 IO 。赋予用户在不修改代码的情况下改变算法或引入新算法的能力，极大地提高了库的可扩展性和可重用性。

\textbf{Serialization} 如前所述，序列化可以帮助存储进程的状态，以便以后恢复它。这是一个有用的功能，但在许多情况下是不必要的。我们的目的是在不改变IO设计的情况下使用一个现有的库（如Boost Serialization）。毕竟数据库的方法总是可以在程序中加入保存和加载功能。

\subsection{Designing the IO} 

在上一节中，对不同的IO功能做了快速回顾。还简要介绍了它们在设计和实现中的影响，最后准备了一个`list`的选定功能。下一步是设计一个IO模块，考虑所有这些功能。

\subsubsection{Multi Formats Support} 

让我们以多格式支持功能来开始设计过程。第一个目标是制作一个可扩展的多格式 IO 。如前所述，正确的设计是建立一个接口，并将每个格式支持封装在一个单独的类中。战略模式是我们正在寻找的。正如在第3.4.1节中提到的，这种模式定义了一系列的算法，并通过单独封装每个算法使它们可以互换，这就是我们要找的。将这种模式应用于我们的问题，会产生如图IO所示的结构 $9.23$ 。





\subsubsection{Multi Media Support} 

多媒体支持可以作为设计的第二个组成部分加入 IO 。第一种方法是假设每一种媒介都可以用来存储任何格式，没有依赖性。在这种情况下，`bridge`模式可以用来将这两个概念解耦，并切断它们的依赖关系。将这种模式引入到之前的设计中，会产生如图[9.24] 所示的结构。

另一种方法是把媒体当作格式，把它们放在同一个结构中。这种方法大大简化了结构和它的实现。除了这个优点外，它使格式和媒体之间有很强的关系，在两个媒体中拥有相同的格式可能会重复编码。图[9.25]显示了这种简单的修改。

这似乎太死板了，但它可以通过一些小的修改而变得更加灵活。另外，`bridge`模式所保证的介质和格式接口的独立性在我们的案例中并不十分必要，而且是以结构复杂化为代价的。在实践中，大量的有限元格式被用来创建文件，而其他类型的媒体被用来与其他类型的格式一起工作。因此，一个简化的方法是使用第二个较简单的结构，并通过模板或多个层次来丰富它，如图所示 [9.26] 。

\subsubsection{Multi IO support} 

这种结构本身就提供了多 IO 个支持。在任何需要IO语句的地方，人们可以创建任何IO对象并执行其IO，而无需重复代码。此外，在执行时提供一些控制可以减少冲突。唯一的例外是，与Kratos的分层性质有关，IO语句不能出现在`Node`、`Element`或`Conditions`中，因为它违反了层序，因为它要求将上层对象放在下层对象中。














\subsubsection{Multi Types and Concepts} 

下一个要添加的功能是对新类型和新概念的可扩展性。如前所述，这是一个通用的多学科库的基本特征。在这里，变量基础接口很方便，它被用来泛化 IO 。

第一步是提供IO对新概念的扩展性。正如前面所描述的，这可以通过引入一个查询表来实现，该查询表与概念名称及其内部处理程序有关。一个简单的`Variables`的`list`就足以让IO作为其查询表。每个`Variable`都知道它的名字和它的参考号。在读取时，IO会读取一个标签，并在`list`中搜索名称与该标签一致的`Variable`。然后使用该变量在数据结构中存储标签值。例如，当IO从输入中读取 "TEMPERATURE=418.651" 语句时，取 "TEMPERATURE" 标签并在`list`中搜索，找到`TEMPERATURE`变量。有了这个变量，就可以使用数据结构的变量基础接口来存储其中的值。对于写入结果，不需要在表中搜索。 IO可以使用该变量在数据库中获取其值，并使用其名称作为标签。这里是`WriteNodalResults`方法的一个例子。

~~~C++
template <class TDataType>
void WriteNodalResults(Variable<TDataType> const &rVariable,
                       NodesContainerType &rNodes,
                       std::size_t SolutionStepNumber)
{
    for (NodesContainerType ::iterator i_node = rNodes.begin();
         i_node != rNodes.end(); ++i_node)
Output << rVariable.Name ()
<< "="
<< i_node ->GetSolutionStepValue(rVariable ,
SolutionStepNumber) )
<< std:: endl;
}

~~~



现在IO可以扩展到新的概念，但是新的`Elements`和`Conditions`呢？每个新的`Element`或`Condition`也是一个新类型，这意味着IO不知道如何创建它。查找表的想法再次派上用场。在这里，我们需要为每个`Element`或`Condition`以及它们的相对工厂方法建立一个表格。 `Prototype`模式有助于以一种通用的方式管理这种情况。在这里，我们通过给每个对象添加一个`Clone`方法来重用它的原型。图[9.27]显示了`Element`所使用的原型机制 IO 。





 IO 使用查找表来查找任何组件名称的对象原型。这个表由代表性的名字和它们相应的原型组成。封装这个表在我们的结构中引入了新的 `KratosComponents` 类。 `KratosComponents`类以一种通用的方式封装了一个类家族的查找表。原型必须以独特的名字添加到这个表中，以便被 IO 所访问。这些名字可以使用C++ `RTTI` 自动创建，也可以为每个组件手动给出。在这个设计中，我们选择了手动的方法，这样可以给每个组件提供更短、更清晰的名字，也可以灵活地给一个对象的不同状态提供不同的名字，并通过不同的原型创建它们。例如，`TriangularThermalElement`和`QuadrilateralThermalElement`都是2DThermalElement的不同实例，初始化为一个三角形或一个四边形。

这种结构允许我们仅仅通过知道它的代表名称来创建任何注册对象。但有时，知道一个对象所属的族是很有用的。例如在读取`Elements`时，没有必要在`Variables`和`Conditions`中搜索，把它们全部放在一起会不必要地减慢解析过程。将查找表划分为三个族类。 `Variables`、`Elements`和`Conditions`有助于在搜索的时候区分它们。这样做也消除了不必要的类型转换，使实现更容易、更清晰。图[9.28]显示了产生的结构。

不幸的是，使用它们的原型名称存储`Elements`和`Conditions`需要每个`Element`和`Condition`存储它们的名称。这引入了不必要的开销，最好是使用`RTTI`而不是手动原型。

接受新类型和多格式支持之间产生了冲突。这个限制来自于格式，它不仅要支持该类型，而且要向 IO 指示该类型。事实上 `Elements` 和 `Conditions` 提供了一个统一的初始化接口，这对于不同的数据值是不可能的。所以`Variable`可以创建数据或从一些现有的来源克隆数据，但IO到`set`的值需要其类型信息。然而在有限元格式中，通常只有少数类型可用，人们可以在实现输入时分别处理它们。

\subsubsection{Serialization Support} 

如前所述，这个功能为程序提供了新的能力，但在我们的设计中，它并不是IO的一个非常重要的部分。串行化可以单独实现，与我们的设计并行。因此，有可能在不改变我们设计的情况下使用外部序列化库，如图所示 [9.29] 。

\subsubsection{`Process` IO} 

通过 IO 理解一个给定的过程需要一个解释器，它动态地构建算法语句并执行它们。解释器的概念和它的设计将在后面描述。但这里要提到的重要一点是在程序内部或外部进行解释的区别。

我们可以在任何IO子类中实现一个解释器，并在IO模块的概念中使用它。这样，任何使用这个库的应用程序都可以使用解释器并接受脚本，而无需调用任何其他可执行文件。同时在IO中封装解释器，允许同时实现和使用多个解释器。这允许用户使用用不同于主脚本本身的语言编写的子程序。

另一种方法是实现或使用一个像应用程序一样的解释器，并与我们的库紧密结合。在这种方法中，更复杂的解释器可以更容易地被使用，而甚至不需要编译它们。这使得库的可移植性更强，更容易编译。

最后，在实践中，这两种方法之间没有这样的区别。两种情况下的实现成本或多或少都是一样的。此外，现代的解释器可以运行另一个解释器，这使得是否将它们放在IO中并不重要。通常情况下，将小型解释器放在 IO 内，并将复杂的解释器作为一个外部应用程序进行绑定，这样做更好。在Kratos中，这两种方法都被使用，然而这在将来很容易被改变。

在设计了IO的主要结构后，现在是时候通过更多的细节了。第一部分是输入界面的设计，接下来是输出界面的描述。

\subsubsection{Designing an Input Interface} 

从有限元应用程序的全局方案可以看出，`Nodes`、`Properties`、`Elements`、`Conditions`和初始值是常见的输入数据。因此，一个直接的输入接口设计包括从输入源提取这些对象的方法。然而，提供这些方法就足以开始工作了，但仍然可以对接口进行更多的扩展，以方便其使用。在一个有限元程序中，读取数据通常包括填充一些有限元容器，如`Mesh`或Modelpart。因此，包括一些直接处理这些容器的方法是很有用的，它可以简化IO的使用，在某些情况下还可以提高性能。如前所述，IO类代表了要在派生类中实现的接口。其接口的输入部分定义如下:

\textbf{`ReadNode`} 从输入中检索下一个`Node`并停止读取。这在手动读取特殊的 `Nodes` 时很有用。这个方法得到一个要读的 `Node` 的参考，并通过输入信息设置它。

~~~C++
ReadNode(NodeType& rThisNode)
~~~

\textbf{ReadNodes} 读取一个源中所有的`Nodes`，直到达到序列的终点。使用这种方法可以对`Nodes`数组进行正常读取。它得到一个`Nodes`数组，并将所有输入的`Nodes`放入其中,
~~~C++
ReadNodes(NodesContainerType& rThisNodes)
~~~


\textbf{ReadProperties} 从输入中读取下一个`Properties`并停止读取。这对更新一个`Properties`很有用。例如，在一个优化程序中读取一个修改过的`Properties`。它需要一个`Properties`，并把给定的信息放在里面。
~~~C++
ReadProperties(Properties& rThisProperties)
~~~



\textbf{ReadProperties} 从输入中读取不同的`Properties`可以通过这个方法完成。这个方法读取所有的`Properties`，直到序列的结束。它需要一个`Properties`的数组，并将所有给定的`Properties`放入其中,
~~~C++
ReadProperties(PropertiesContainerType& rThisProperties)
~~~


\textbf{ReadElement} 读取输入中的下一个`Element`。这个方法识别在Kratos中注册的任何`Element`，并读取第一个给定的输入。这个方法需要一个`Nodes`和`Properties`的数组来提取必要的信息来创建一个`Element`。它还需要一个 `Element` 指针并将其分配给新创建的 `Element` 。
~~~C++
ReadElement(NodesContainerType &rThisNodes,
            PropertiesContainerType &rThisProperties,
            Element ::Pointer pThisElement)
~~~


\textbf{ReadElements} `Elements` 读取方法。这个方法识别在Kratos中注册的任何`Element`，并读取输入中给出的所有`Elements`。这个方法需要一个`Nodes`和`Properties`的数组来提取必要的信息以创建`Element`。它还需要一个`Elements`数组，以便将创建的`Elements`放入其中,
~~~C++
ReadElements(NodesContainerType &rThisNodes,
             PropertiesContainerType &rThisProperties,
             ElementsContainerType &rThisElements)
~~~



\textbf{ReadCondition} 读取输入序列中的下一个`Condition`。和ReadElement方法一样，需要一个`Nodes`和`Properties`的数组来提取必要的信息来创建一个`Condition`。它还需要一个 `Condition` 指针，如果读取的 `Condition` 不是 `Dirichlet` 类型，它就把它分配给新创建的 `Condition` 。ReadConditions `Conditions` 读取方法。这与ReadElements非常相似，但要寻找`Conditions`而不是`Elements` 。这个方法也读取 `Dirichlet` 类型的条件作为输入。同样，像ReadElements一样，它需要 `Nodes` 和 `Properties` 的数组来创建 `Conditions` 。生成的`Conditions`被放置在给定的`Conditions`阵列中。
~~~C++
ReadCondition(NodesContainerType &rThisNodes,
              PropertiesContainerType &rThisProperties,
              Condition ::Pointer pThisCondition)
~~~


\textbf{ReadInitialValue} 从输入中读取下一个初始值并将其放入数据结构。它取 `Nodes` , `Elements` 和 `Conditions` 并给它们分配初始值。

~~~C++
ReadConditions(NodesContainerType &rThisNodes,
               PropertiesContainerType &rThisProperties,
               ConditionsContainerType &rThisConditions)
~~~

\textbf{ReadInitialValues} 读取所有初始值并将其放入数据结构中。它采取`Nodes`、`Elements`和`Conditions`来分配输入的初始值。
~~~C++
ReadInitialValues(NodesContainerType &rThisNodes,
                  ElementsContainerType &rThisElements,
                  ConditionsContainerType &rThisConditions)
~~~


\textbf{ReadMesh} 读取所有 `Nodes` , `Properties` , `Elements` 和 `Conditions` 并将它们储存在给定的 `Mesh` 中。这种方法是在程序开始时填充 `Mesh` 的最简单方法。
~~~C++
ReadMesh(MeshType & rThisMesh)
~~~


\textbf{ReadModelPart} 读取所有`Nodes`、`Properties`、`Elements`和`Conditions`并将它们存储在给定的`ModelPart`中。它还初始化了 `ModelPart` 的名称和它的属性。

~~~C++
ReadModelPart(ModelPart & rThisModelPart)
~~~

值得一提的是，上述方法只是接口，每个派生类可能实现所有的或只是其中的一组。所以为了避免意外的问题，在所有的IO方法中加入错误信息，并告知用户在派生类中调用一些未实现的功能是很有用的。

~~~C++
virtual void ReadModelPart(ModelPart &rThisModelPart)
{
    KRATOS_ERROR(std::logic_error,
                 "Calling base class member. Please check the ...", "");
}
~~~



\subsubsection{Designing the Output Interface} 

输出接口是由IO定义的，输出格式被封装在I0派生类中。该接口的第一部分专门用于编写基本对象。这些方法对于写一个重启文件是有帮助的。此外，还提供了写有限元容器的方法。这些方法在导出模型或给后处理程序提供额外信息方面很有用。最后，还有一部分界面是专门用来写结果的。下面是可用的输出方法的`list`。

\textbf{WriteNodes} 将给定的`Node`写入输出中。如果输出格式需要，该方法也可以写入节点数据。

~~~C++
WriteNode(NodeType const& rThisNode)
~~~


\textbf{WriteNodes} 这个方法将一个`Nodes`的数组写进输出。它也可以用来创建重启文件。它的参数只是一个`Node`容器。

~~~C++
WriteNodes(NodesContainerType const& rThisNodes)
~~~


\textbf{WriteProperties} 取一个`Properties`并将其写入输出。这个方法对于交流一些`Properties`或通过接口交流很有用。
~~~C++
WriteProperties(Properties const& rThisProperties)
~~~



\textbf{WriteProperties} 将所有给定的`Properties`写到输出端。取一个`Properties`容器并写入所有的容器。这个方法可以用来创建重启文件，给写所有的`Properties`在里面。
~~~C++
WriteProperties(PropertiesContainerType const& rThisProperties)
~~~

\textbf{WriteElement} 将一个`Element`写进输出。它需要一个`Element`作为它的参数。在 `Elements` 之间进行选择性写入是很有用的。

~~~C++
WriteElement(Element const& rThisElement)
~~~

\textbf{WriteElements} 这个方法需要一个`Elements`的容器并将它们全部写入输出。所以它是一个有用的方法，通过传递所有的`Elements`来写一个重启文件。

~~~C++
WriteElements(ElementsContainerType const& rThisElements)
~~~

\textbf{WriteCondition} 将给定的 `Condition` 写入输出。

~~~C++
WriteCondition(Condition const& rThisCondition)
~~~

\textbf{WriteConditions} 取一个`Nodes`的容器，写出分配给每个`Node`的`Dirichlet`条件。这个方法可以用来写重启文件。
~~~C++
WriteConditions(NodesContainerType const& rThisNodes)
~~~


\textbf{WriteConditions} 上一个方法的重载，它接收一个`Conditions`的容器并将它们写入输出。这个方法也适用于写重启文件。
~~~C++
WriteConditions(ConditionsContainerType const& rThisConditions)
~~~



\textbf{WriteMesh} 这个方法将所有`Nodes`、`Properties`、`Elements`和`Conditions`中给定的`Mesh`写到输出中。这使得它成为编写重启文件的最佳方法之一，或者也可以用来为后期处理任务创建额外的信息。

~~~C++
WriteMesh(MeshType const& rThisMesh)
~~~

\textbf{WriteModelPart} 将指定的`ModelPart`的所有`Nodes`、`Properties`、`Elements`和`Conditions`写到输出中。这个方法也把`ModelPart`的属性放到输出中。
~~~C++
WriteModelPart(ModelPart const& rThisModelPart)
~~~


\textbf{WriteNodalResults」。该方法将所有给定`Nodes`的`Variable`的节点值写入输出。这个方法可以用来为后期处理写节点结果。它需要写入的变量和一个必须写入其值的`Nodes`的容器。它还需要一个`Process`的信息，该信息为写入结果提供额外的信息，如时间步长。
~~~C++
WriteNodalResults(VariableData const &rVariable,
                  NodesContainerType &rNodes,
                  ProcessInfo &rCurrentProcessInfo)
~~~

\textbf{WriteElementalResults} 这个方法将给定变量的元素值写进输出。这个方法可以用来为后期处理写入元素结果。它需要被写入的变量和一个必须写入其值的`Elements`的容器。此外，它还需要一个`ProcessInfo`，为写结果提供额外的信息，如时间步骤。

~~~C++
WriteElementalResults(VariableData const &rVariable,
                      ElementsContainerType &rElements,
                      ProcessInfo &rCurrentProcessInfo)
~~~

\textbf{WriteIntegrationPointsResults} 这个方法将给定的`Elements`的积分点上计算的结果写入输出。它需要写入的变量和一个必须写入其积分点值的`Elements`容器。此外，它还需要一个`Process`的信息，该信息为写入结果提供额外的信息，如时间步长。
~~~C++
WriteIntegrationPointsResults(VariableData const &rVariable,
                              NodesContainerType &rNodes,
                              ProcessInfo &rCurrentProcessInfo)
~~~



这里有一个重要的细节需要提及。可以看到，最后三个方法都是以`VariableData`为参数，而不是以类型变量或组件为参数。在$\mathrm{C}++$中，模板函数不能被定义为虚拟。这使得我们无法将这些方法变成一个模板。因此，为了让它仍然可以扩展到新的类型，接口只是提供了一个非常通用的接口，每个实现都必须通过`RTTI`检查变量的类型，然后将其分配给适当的实现。

\subsection{Writing an Interpreter} 

本节介绍了一步步编写解释器的方法。 首先，全局结构及其组成部分将被注释。然后描述了通过使用两种不同的工具来创建一个解释器。

\subsubsection{Global Structure of an Interpreter} 

解释器通常被分为两部分，如图所示 [9.30] 。

第一部分是 `lexical analyzer` 。这一部分读取输入字符，例如源文件或给定的命令行语句，并将其转换为可用于 `parser` 的一系列 `tokens` ，如数字、名称、关键词等。由于空白（如空白、制表符和换行符）和注释不用于解析代码，因此`lexical analyzer`有一个次要任务，即在分析过程中消除所有空白和注释。`lexical analyzer`也可以为`parser`提供额外的信息，以产生更具描述性的错误信息。

令牌是具有逻辑意义的字符序列，或在我们的语法中构成一个单元。人们可以根据所使用的语法，将`tokens`分为不同的类别。[表9.3] 显示了一些 `tokens` 的例子。



如果一个以上的输入字符序列与一个标记相匹配，那么这个标记必须提供一个值域来存储它所代表的输入数据。为了澄清令牌和`lexical analyzer`的概念，让我们举一个例子。考虑到一个简单的$\mathrm{C}$if语句。

~~~C++
if(x > 0.00)
    return 0;

~~~

将其传递给C `lexical analyzer` 将导致 `tokens` 的序列。

~~~C++
IF LPARENTHESES ID(x) GREATER REAL(0.)
RPARENTHESES RETURN INTEGER(0) SEMICOLON
~~~


第二部分是 `parser` 。解析器做语法分析。从`lexical analyzer`中获取`tokens`，并试图根据其语法找到它们的关系和意义。解析器通过将认可的语句和它们的子操作放在一起产生一个解析树。这个树可以用来执行给定的源。例如，将上述`tokens`的序列传递给`C parser`会产生一个如图[9.31]所示的解析树。

将 `lexical analyzer` 与 `parser` 分开有几个原因。第一个是简化设计，减少 `parser` 的复杂性。在分离的`parser`上创建一个没有任何注释或空白的`tokens`，比在输入字符上创建一个`parser`要容易。第二个原因是提高解释器的性能。大量的解释器时间花在做词法分析上，将其分离并使用缓冲等技术可以显著提高性能。另一个原因是与解释器的可移植性和重复使用能力有关。任何由于不同设备中的不同字符映射而产生的问题都可以封装在 `lexical analyzer` 中，而不需要改变 `parser` 。

在某些情况下，`lexical analyzer`也被分为两个阶段。一个是做简单任务的扫描器，如删除注释和空白，另一个是做真正复杂工作的分析器。



现在让我们进入每个部分，看看如何实现它。

\subsubsection{Inside a Lexical Analyzer} 

一个`lexical analyzer`使用一些模式来寻找给定的字符序列中的`tokens`。通常，这些模式是由 `regular expressions` 指定的。因此，在开始使用`lexical analyzer`模式之前，有必要对`regular expressions`进行简要说明。

\textbf{Regular expressions} 

一种语言可以被看作是具有某些特殊属性的字符串的`set`。因此，准确描述这些属性对于定义一种语言是很重要的。正则表达式是以通用方式定义这些概念的常用符号。

这个符号由一些规则组成，这些规则让正则表达式$r$表示语言$L(r)$，还包括一些其他规则，以不同的方式将不同的表达式组合在一起。这种组合规则使得它在将复杂的定义划分为更简单的定义或重复使用一些表达式来制作更复杂的定义方面非常强大。

正则表达式规则由符号集合规则组成，这些符号来自一个叫做字母表的 `set` 符号，正则表达式是在字母表上定义的。下面是在字母表 $\Sigma$ 上定义正则表达式的规则。

\textbf{Epsilon} $\epsilon$ 是一个正则表达式，表示只包含空字符串的语言 $\{\epsilon\}$ 。

\textbf{Symbol} 对于$\Sigma$中的符号$a$，正则表达式$a$表示语言$\{a\}$，其唯一字符串是a。

\textbf{Alternative} 用竖条写的替代运算符|将两个 `regular expressions` $r$ 和 $s$ 组合成新的正则表达式 $r \mid s$ 。这个表达式代表语言 $L(r) \bigcup L(s)$ ，它包含了 $r$ 或 $s$ 所包含的所有字符串。例如，$a \mid b$代表语言$\{a, b\} .$ 。

\textbf{Concatenation} 两个 `regular expressions` $r$ 和 $s$ 可以使用连接操作符 $\cdot$ 组合成一个新的正则表达式 $r \cdot s$ 。这个新的表达式表示语言 $L(r) L(s)$ ，它包含所有 $\alpha \in L(r)$ 和 $\beta \in L(s)$ 的所有字符串 $\alpha \beta$ 。例如，$a \cdot b$ 表示语言$\{a b\}$ 。在一些符号中，$\cdot$符号被简单地省略了，把两个表达式简单地一个接一个表示串联，$a \cdot b \equiv a b$ 。

\textbf{Repetition} 重复运算符 $^{*}$ ，或克莱因星，应用于正则表达式 $r$ 表示语言 $L(r)^{*}$ 包含有 $L(r)$ 中任何符号的零或多个实例的字符串。例如，$(a \mid b)^{*}$ 代表语言 $\{\epsilon, a, b, a a, a b, b a, b b, a a a, a a b, \ldots\}$ 。

\textbf{Parentheses} 小括号可以用来分组操作，将其应用于单个规则不会改变表达规则。

在运算符的优先级方面也有一些约定，有助于减少歧义。

- Kleene star的优先级最高。

- 连合的优先级为第二。

- 替代运算符的优先级最低。

- 所有这些运算符都是左联的。

最后，有一些频繁表达式的缩写，以方便在实践中使用。

\textbf{Positive closure} 正闭运算符$+$是左联动的，应用于正则表达式$r$，表示包含$L(r)$中任何符号的一个或多个实例的语言$L(r)^{+}$。该操作符与重复操作符 $^{*}$ 具有相同的优先权。 $r^{*}=r^{+} \mid \epsilon$ 和 $r^{+}=r r^{*}$ 公式描述了正闭合运算符和重复运算符之间的关系。例如，$(a \mid b)^{+}$代表语言$\{a, b, a a, a b, b a, b b, \ldots\}$ .

\textbf{Zero or one instances} 对正则表达式$r$应用单数运算符？ 表示语言$L(r) \bigcup\{\epsilon\}$，其中包含$L(r)$中任何符号的零或一个实例。这个运算符是$r \mid \epsilon$的缩写。例如，$(a \mid b)$ ? 表示语言$\{\epsilon, a, b\}$ 。

\textbf{Character classes} 对于字母符号 $a, b, \ldots, z$ 符号 [a b c] 相当于 $a|b| c$ ， [a-z] 相当于 $a|b| \ldots \mid z$ 。这两个缩写也可以合并。例如，一个C标识符可以是任何字母，后面跟着零个或多个字母或数字。这个定义可以用正则表达式 $[A-Z a-z][A-Z a-z 0-9]^{*}$ 来写。

在简单描述了 `regular expressions` 之后，现在是时候看一下 `lexical analyzer` 本身了。

\textbf{Lexical Analyzer} 

如前所述，`lexical analyzer`负责使用一些模式将一串字符作为其输入转换为一串`tokens`作为其输出。通常，`regular expressions`被用来指定这些模式。例如，阅读以下节点语句。

~~~C++
// A typical Node Statement
Node (1 ,1.00 ,0.00 ,0.00)
~~~



`lexical analyzer`需要理解`Node`关键字、整数、实数小括号以及注释。下面是`regular expressions`定义合法的`tokens`和它们在这种情况下的相应动作。

~~~C++
Node                                    {return NODE_TOKEN}
[0 -9]+                                 {return INTEGER_TOKEN}
([0 -9]+"."[0 -9]*)|([0 -9]*"."[0 -9]+) {return REAL_TOKEN} "//"("
"|[A-Za-z0 -9])*"\n"                    {/* It is just comment */} "
"|"\n"|"\t"                             {/* white spaces */}
~~~



这些规则在某种程度上是模糊的。例如，阅读$\mathrm{x}$坐标$1.00$会产生一个问题，因为第一个1可以被识别为一个整数，而$.00$则是它后面的一个`double`！为了减少这种含糊不清的词法，`regular expressions`定义了合法的`tokens`及其相应的动作。为了减少这种模糊性，词法分析器使用了两个额外的规则。

\textbf{Longest match} 分析器试图找到能与其中一个模式匹配的最长的输入字符序列。

\textbf{Pattern priority} 对于一个特定的最长的字符串，与之匹配的第一个正则表达式有优先权来确定其类型。

这里的想法是创建一个机制，将给定的`regular expressions`自动转换为`lexical analyzer`。该方法是将给定的`regular expressions`转换为一个可以轻松实现的图。

 首先，正则表达式必须被转换为 `finite automata` 。`finite automata` 是一个由有限`set`的状态组成的图，由边连接，每条边都标有一个符号。这个图显示了一个字符序列可以穿越所有的状态，与一个正则表达式相匹配的方式。如果有限自动机的所有状态中最多只有一条边是为一个符号出去的，那么它就是确定性的，任何状态中的符号都有一条以上的出去的边，那么它就转化为非确定性的`finite automata`.图[9.32]显示了前面例子中的`finite automata`。

现在让我们把所有这些独立的`finite automata`合并成一个，代表我们的`lexical analyzer`。对于这个小例子来说，这可以手动完成，但在实践中，手动组合是不可能的。幸运的是，有一些方法可以自动做到这一点。这些方法包括将`regular expressions`转换为非确定性的`finite automata`（NFA），然后将其还原为确定性的`finite automata`（DFA），并进行一些优化。关于将 `regular expressions` 自动转换为确定性 `finite automata` 的程序的详细信息可以在 [^12][^13][^16] 找到。在这里，手动组合被用来创建图[9.33] 中所示的完整图形。

最后这个组合图可以转换为一个 `transition table` 。这个表格由几行组成，每一个状态都有一个，也有几列，每个符号都有一个。每个字段都包含其行状态的目标状态号码，并给出其列符号。

有了`transition table`，分析算法就非常容易实现。程序首先读取一个字符，然后进入对应于状态1的行和这个符号的列的字段。这个字段包含了目标状态，比如说状态7。然后它读取下一个字符，并进入状态7的行和这个新符号的列，以找到下一步，如此循环。一个辅助数组将状态号码映射到动作上也是必要的，以验证如果一个模式被匹配，要执行的动作是什么。为了找到最长的匹配，只需要存储最后发现的匹配，并在每次发现新的匹配时更新它。这个过程一直持续到达到一个死亡状态。在这个时候，如果有的话，就会返回最后一个匹配案例，如果没有匹配，就会发送一个错误。

\subsubsection{Parser} 

一个`lexical analyzer`为`tokens`做准备，以便被`parser`分析并创建要执行的解析树。`parser`试图将给定的`tokens`序列与一个模式匹配。这里使用了一个上下文自由语法来指定模式。因此，在继续进行`parser`之前，让我们看看它是什么。

\textbf{Context Free Grammar} 

这种符号的想法是将基本部分归入一些更复杂的部分，如`regular expressions`，同时对符号进行递归使用，以实现复杂语句的递归构建。

一个上下文自由语法由终端、非终端、生成物和一个开始符号组成。

\textbf{Terminal} 终端是语法的基本单位。在编程语言的语法中，终端等同于 `tokens` 。在我们的例子中，每个标记都是上下文自由语法中的一个终端。

\textbf{Nonterminal} 非终端是语法变量，代表一组终端或非终端，是语言语法的一部分。

\textbf{Production} 生产是通过终端和非终端的组合来定义非终端的方式。生产的一般形式是：。

\begin{equation}
\text { nonterminal } \rightarrow \text { sting of terminals and nonterminals }
\end{equation}

在一些符号中，用符号$::=$代替了箭头。

\textbf{Start Symbol} 它是一个非终端，被选作起始符号，表示语法所定义的语言。

为了使这个定义清楚，让我们创建一个语法来定义前面的节点输入文件语言。

- $statement \rightarrow statement \mid node_creator$ 

- $node creator \rightarrow Node (integer, coordinates)$ 

- $coordinates \rightarrow real, real, real$ 

第三行创建了一个用逗号分隔的三个实数的坐标模式。第二行给出了构建一个 `Node` 的模式。最后，第一行定义了构建`Node`或其本身的语句。这导致 `parser` 递归并读取所有节点语句之后的语句。

可以验证，每个正则表达式`set`本身就是一个上下文自由语法。这可能会带来一个问题，为什么使用正则表达式来定义词汇模式而不是上下文自由语法本身。这里有一些原因。

- 正则表达式足以定义语法，不需要处理无语境语法的复杂性。

- 正则表达式更容易理解，它们对标记的定义更清晰。

- 当通过正则表达式而不是通用语法构造时，词汇分析器可以自动优化，使其更有效率。

\textbf{Designing a Parser} 

要设计一个`parser`，`Interpreter`模式是我们要寻找的。一个上下文自由结构可以用这个结构来表示，然后用来创建解析树来执行源代码。一般来说，在文献 [^12][^13][^16] 中有各种方法来实现 `parser` 。也有一些工具可以生成 `parser` 。这里我们将使用这些工具，而不去研究 `parser` 的实现细节。

\subsubsection{Using `Lex` and `Yacc`, a Classical approach} 

有几个工具可以创建一个 `parser` 。其中，`Lex`和`Yacc`是两个经典的工具。

 `Lex` 是一个`lexical analyzer`生成器。词汇规范包含定义每个标记的正则表达式，以及当输入序列与正则表达式匹配时将会执行的操作。这些动作是正常的C语句，通常会返回其情况的标记。`Lex`的落后之处在于其性能。 `Fast lexical analyzer generator`（Flex）明显比`Lex`[^16]快，解决了性能问题，但它仍然会产生C代码。虽然将C代码绑定到$\mathrm{C}++$程序中没有问题，但这仍然会因为使用几个全局变量和函数而污染了范围。这也使得在同一代码中维护一个以上的`lexical analyzer`变得困难。在这项工作中，一个名为$\mathrm{C}++$的Flex变体被用来产生$\mathrm{Flex}++$。

 `Yet Another Compiler-Compiler` （ `Yacc` ）是一个经典的和广泛使用的`parser`生成器。有几个编译器使用`Yacc` 实现。 `Bison`是另一个`parser`生成器，它比`Yacc`更现代，但与它向上兼容。两者都生成了用C语言实现的解析器。在这项工作中，使用了`Bison++`，它是Bison的一个变种，可以生成用$\mathrm{C}++$实现的解析器。

\textbf{Using Flex} 

Flex++与`Lex`一样，使用词法规范来生成`lexical analyzer`。这种规格必须通过创建一个通常以 "1 "为扩展名的输入文件来准备，如 "input.l"，并由三部分组成，以$\% \%$ 分隔。

~~~C++
%{
C++ Definitions
%}
Lex Definitions

%%

Regular expressions and their actions

%%

Auxiliary procedures

~~~


第一部分是定义，也分为两部分，$C++$定义部分，它被包围在\%{ \%}符号和`Lex` 定义部分之间。 `FLex++`只是把$C++$声明部分放在生成的输出文件上面。这一部分通常包含必要的包含文件和一些宏定义来定制生成的代码。在我们的例子中，$\mathrm{C}++$的声明部分是。

~~~C++
%{
#define YY_DECL \
    int Kratos :: InputCScanner :: yylex(yy_InputCParser_stype &yylval)
#include <iostream >
#include <malloc.h>
#include "includes/input_c_parser.h"
#include "includes/input_c_scanner.h"
int yyFlexLexer :: yylex (){ return 0; }
%}

~~~


lex定义部分是把用于lex或条件的宏放在后面使用。下面是一个宏定义的例子。
~~~C++
DIGIT    [0-9]
ID       [a-z][a-z0 -9]*
~~~

在这种情况下，这里只定义了一个评论的条件。

~~~C++
%x COMMENT
~~~

第二部分是定义规则。每个规则都是一个正则表达式和其相应的动作。在 Kratos 中定义了四组规则。

- 处理注释的规则。C++注释以这种格式被接受。这里的一个要点是在注释中保留行号。另一点是在注释中出现注释的情况下给出警告。

~~~C++
\n                            { ++ mNumberOfLines; }
"//".*$                       /* remove one line coments */
"/*"                          { BEGIN COMMENT; }
<COMMENT >"/*"                { Warning("Comment in comment."); }
<COMMENT >\n                  { ++ mNumberOfLines; }
<COMMENT >[^*/\n]* ;
<COMMENT >"*"+[^*/\n]* ;
<COMMENT >"/"+[^*/\n]* ;
<COMMENT >"*"+"/"             { BEGIN INITIAL; }

~~~


- 第二组是符号。它们中的大多数都是按原样传递的。在更正式的方式中，每个符号必须由一个代表符号来传递，为了简单起见，这里没有这样做。

~~~C++
\[|\]|"("|")"|"{"|"}"                   { return yytext [0]; }
"="                                     { return yytext [0]; }
","|";"|"."|":"                         { return yytext [0]; }
"<"|">"                                 { return yytext [0]; }
"=="                                    { return InputCParser :: EQUAL_TOKEN; }
"!="                                    { return InputCParser :: NOT_EQUAL_TOKEN; }
"<="                                    { return InputCParser :: LESS_EQUAL_TOKEN; }
">="                                    { return InputCParser :: GREATER_EQUAL_TOKEN ;}
"+"|"-"                                 { return yytext [0]; }


~~~

- 在符号之后，有一些规则来定义Kratos变量。下面是这些规则的一些例子。

~~~C++
TEMPERATURE {
                yylval.double_variable = &TEMPERATURE;
                return InputCParser :: DOUBLE_VARIABLE_TOKEN;
}
VELOCITY {
                yylval.vector_double_variable = &VELOCITY;
                return InputCParser :: VECTOR_DOUBLE_VARIABLE_TOKEN;
}

~~~


- 最后是关键词的规则。
~~~C++
NODES               { return InputCParser :: NODES_TOKEN; }
PROPERTIES          { return InputCParser :: PROPERTIES_TOKEN; }
Node                { return InputCParser :: NODE_TOKEN; }
Fix                 { return InputCParser :: FIX_TOKEN; }
ElementsGroup       { return InputCParser :: ELEMENTS_GROUP_TOKEN; }
ELEMENTS            { return InputCParser :: ELEMENTS_TOKEN; }
LinearSolver        { return InputCParser :: LINEAR_SOLVER_TOKEN; }
SolvingStrategy     { return InputCParser :: SOLVING_STRATEGY_TOKEN; }
Smooth              { return InputCParser :: SMOOTH_TOKEN; }
Map                 { return InputCParser :: MAP_TOKEN; }
DataMapper          { return InputCParser :: MAPPER_TOKEN; }
for                 { return InputCParser :: FOR_TOKEN; }
Solve               { return InputCParser :: SOLVE_TOKEN; }
MoveMesh            { return InputCParser :: MOVE_MESH_TOKEN; }
Print               { return InputCParser :: PRINT_TOKEN; }
Execute             { return InputCParser :: EXECUTE_TOKEN; }
CreateSolutionStep  {return InputCParser :: CREATE_SOLUTION_STEP_TOKEN; }
CreateTimeStep      { return InputCParser :: CREATE_TIME_STEP_TOKEN; }
~~~







也有一些其他的规则定义，以刮白的空间，我们返回不匹配的话。Flex输入的第三部分是把辅助程序，在这种情况下是留空的。Flex++利用这些信息来生成过渡表和通用代码，以便以后由`Bison++`调用。

\textbf{Using Bison++} 

与Flex++一样，Bisont+的规格必须准备在一个文件中，但通常以 "y "为扩展名，如 "input. y"。这个文件同样由三个部分组成，以\%\%分开。

~~~C++
%{
C++ Definitions
%}
Parser Definitions

grammar rules

%%
Auxiliary codes
~~~




这里的C++定义很重要，因为所有抽象树类的定义都在这里。

~~~C++
%{
    Kratos ::String block_name;
    class Statement
    {
    public:
        Statement() {}
        virtual void Execute(Kratos ::Kernel *pKernel) {}
        virtual int Value(Kratos ::Kernel *pKernel) { return 0; }
    };
    class BlockStatement : public Statement
    {
        ...
    };
    class GenerateNodeStatement : public Statement
    {
        ...
    };
    template <class TDataType>

    class GeneratePropertiesStatement : public Statement
    {
        ...
    };
    class SetElementsGroup : public Statement
    {
        ...
    };
    class GenerateElementStatement : public Statement
    {
        ...
    };
    class FixDofStatement : public Statement
    {
        ...
    };
    template <class TDataType>
    class SetSourceStatement : public Statement
    {
        ...
    };
    class GenerateLinearSolverStatement : public Statement
    {
        ...
    };
    class GenerateSolvingStrategyStatement : public Statement
    {
        ...
    };
    class GenerateMapperStatement : public Statement
    {
        ...
    };
    class SolveStatement : public Statement
    {
        ...
    };
    class MoveMeshStatement : public Statement
    {
        ...
    };
    class ProcessStatement : public Statement
    {
        ...
    };
    class ForStatement : public Statement
    {
        Statement *mpFirst;
        Statement *mpSecond;
        Statement *mpThird;
        Statement *mpForth;

    public:
        ForStatement(Statement *pFirst, Statement *pSecond,
                     Statement *pThird, Statement *pForth) : mpFirst(pFirst), mpSecond(pSecond),
                                                             mpThird(pThird), mpForth(pForth) {}
        ~ForStatement()
        {
            delete mpFirst;
            delete mpSecond;
            delete mpThird;
            delete mpForth;
        }
        void Execute(Kratos ::Kernel *pKernel)
        {
            for (mpFirst->Execute(pKernel); mpSecond->Value(pKernel);
                 mpThird->Execute(pKernel))
                mpForth->Execute(pKernel);
        }
    };
    template <class TDataType>
    class PrintStatement : public Statement
    {
        ...
    };
    template <class TDataType>
    class PrintOnGaussPointsStatement : public Statement
    {
        ...
    };
    template <class TDataType>
    class SmoothStatement : public Statement
    {
        ...
    };
    template <class TDataType>
    class MapStatement : public Statement
    {
        ...
    };
    class CreateTimeStepStatement : public Statement
    {
        ...
    };
    class CreateSolutionStepStatement : public Statement
    {
        ...
    };
    template <class TFunction, class TDataType>
    class LogicalStatement : public Statement
    {
        ...
    };
    template <class TDataType>
    class ValueStatement
    {
        ...
    };
    template <class TDataType>
    class ConstantValueStatement : public ValueStatement<TDataType>
    {
        ...
    };
    template <class TDataType>
    class VariableStatement : public ValueStatement<TDataType>
    {
        ...
    };
    template <class TFunction, class TDataType>
    class BinaryVariableStatement : public ValueStatement<TDataType>
    {
        ...
    };
    template <class TDataType>
    class AssigningVariableStatement : public Statement
    {
        ...
    };
    template <class TDataType>
    class AssigningNodalVariableStatement : public Statement
    {
        ...
    };
%}

~~~






类的实现被删除以减少文件的大小。第二部分是为`parser`的定义。带有所有 `tokens` 和非终端声明的宏定义被放在这里。

~~~C++
%define CONSTRUCTOR_PARAM Kratos :: Kernel* ...
%define CONSTRUCTOR_INIT : mInputCScanner(pNewInput , ...
%define YY_InputGid_CONSTRUCTOR_CODE
%define LEX_BODY { return mInputCScanner.yylex(yylval ); }
%define MEMBERS Kratos :: InputCScanner mInputCScanner ;...
%token <integer_value > INTEGER_TOKEN
% token <double_value > DOUBLE_TOKEN
%token <string_value > WORD_TOKEN
%token <double_variable > DOUBLE_VARIABLE_TOKEN
%token <vector_double_variable > VECTOR_DOUBLE_VARIABLE_TOKEN
%token <matrix_variable > MATRIX_VARIABLE_TOKEN
%token OPEN_BRACKET_TOKEN
%token CLOSE_BRACKET_TOKEN
%token NODES_TOKEN
%token NODE_TOKEN
%token PROPERTIES_TOKEN
%token ELEMENTS_GROUP_TOKEN
%token ELEMENTS_TOKEN
%token ELEMENT_TOKEN
%token BEGIN_TOKEN
%token END_TOKEN
%token FIX_TOKEN
%token SOURCES_TOKEN
%token FOR_TOKEN
%token SOLVE_TOKEN
%token MOVE_MESH_TOKEN
%token PRINT_TOKEN
%token PRINT_ON_GAUSS_POINTS_TOKEN
%token EXECUTE_TOKEN
%token TRANSIENT_TOKEN
%token ALPHA_TOKEN
%token SMOOTH_TOKEN
%token MAP_TOKEN
%token MAPPER_TOKEN
%token LINEAR_SOLVER_TOKEN
%token SOLVING_STRATEGY_TOKEN
%token EQUAL_TOKEN
%token NOT_EQUAL_TOKEN
%token LESS_EQUAL_TOKEN
%token GREATER_EQUAL_TOKEN
%token CREATE_SOLUTION_STEP_TOKEN
%token CREATE_TIME_STEP_TOKEN

%type <statement_handler > statement
%type <statement_handler > expresion
%type <statement_handler > node_generating
%type <statement_handler > properties_adding
%type <statement_handler > set_elements_group
%type <statement_handler > element_generating
%type <statement_handler > nodes_variables_fixing
%type <statement_handler > nodes_variables_setting
%type <statement_handler > elements_variables_fixing
%type <statement_handler > linear_solver_generating
%type <statement_handler > solving_strategy_generating
%type <statement_handler > mapper_generating
%type <statement_handler > solve_process
%type <statement_handler > move_mesh_process
%type <statement_handler > execute_process
%type <statement_handler > print_process
%type <statement_handler > print_on_gauss_points_process
%type <statement_handler > smooth_process
%type <statement_handler > map_process
%type <statement_handler > step_creating
%type <statement_handler > time_step_creating
%type <statement_handler > for_loop
%type <statement_handler > assignment_expresion
%type <statement_handler > logical_expresion
%type <block_statement_handler > block_generating
%type <block_statement_handler > block
%type <double_value_statement_handler > double_variable_expresion
%type <integer_value > integer_expresion
%type <integer_value > integer_index
%type <integer_value > nodes_array
%type <integer_value > elements_array
%type <integer_value > properties_array
%type <double_value > double_expresion
%type <vector_integer_value > integer_sequence
%type <vector_integer_value > vector_integer
%type <vector_double_value > vector_double
%type <matrix_double_value > matrix
%type <vector_double_value > double_sequence
%type <vector_vector_double_value > vector_double_sequence

~~~






第二部分存放上下文自由格式的语法定义和它们相应的语义动作。这里是用于输入的组织语法。

~~~C++
statement               : expresion ’;’ {$$ = $1}
                        | block {$$ = $1;}
                        | for_loop {$$ = $1;}
                        ;
expresion               : assignment_expresion {$$ = $1;}
                        | logical_expresion {$$ = $1;}
                        | node_generating {$$ = $1}
                        | properties_adding {$$ = $1;}
                        | set_elements_group {$$ = $1;}
                        | element_generating {$$ = $1;}
                        | nodes_variables_fixing {$$ = $1;}
                        | nodes_variables_setting {$$ = $1;}
                        | elements_variables_fixing {$$ =$1;}
                        | solve_process {$$ = $1;}
                        | move_mesh_process {$$ = $1;}
                        | execute_process {$$ = $1;}
                        | print_process {$$ = $1;}
                        | print_on_gauss_points_process {$$ = $1;}
                        | smooth_process {$$ = $1;}
                        | map_process {$$ = $1;}
                        | linear_solver_generating {$$ =$1;}
                        | solving_strategy_generating {$$ = $1;}
                        | mapper_generating {$$ = $1;}
                        | step_creating {$$ = $1;}
                        | time_step_creating {$$ = $1;}
                        ;

block                   : block_generating ’}’
                          {
                                $$ = $1;
                          }
block_generating        : ’{’ statement
                           {
                                $$ = new BlockStatement ();
                                $$ ->AddStatement($2);
                           }
                        |  block_generating statement
                           {
                                $1 ->AddStatement($2);
                                $$ = $1;
                           }

~~~


其余的定义没有放在这里，以减少列表的大小。第三部分是辅助功能。在我们的例子中，这里定义了一个错误报告函数。

~~~C++
using Kratos ::Exception;
void InputCParser ::yyerror(char *s)
{
    Kratos ::String buffer;
    buffer << "Pars error in line no "
           << mInputCScanner.rNumberOfLines()
           << ", last token was : "
           << mInputCScanner.GetYYText();
    KRATOS_ERROR(std::runtime_error, "Reading Input", buffer, "");
}
~~~



\subsubsection{Creating a new Interpreter Using Spirit} 

Flex++和`Bison++`是生成解释器的好工具，但它们在这项工作中被搁置，主要有两个原因。第一个原因是代码开发的策略发生了变化，从创建一个复杂的解释器到使用一个现有的解释器。我们的想法是拥有一个简单而灵活的数据 IO 并使用一个现有的解释器来处理 IO 。顺便说一下，对于读取数据，仍然需要一个简单的`parser`，但这些工具太重了，不能用于这种情况。另一个原因是可移植性和维护问题。语法上的任何小变化，例如引入一个新的变量名，都需要重新生成`parser`，然后重新编译程序。在实践中，这产生了可移植性问题，特别是在Windows，由于这些工具的Linux和Windows编译之间的不兼容。

Spirit是一个面向对象的`parser`生成器框架，在$\mathrm{C}++$中使用模板元编程技术实现。Spirit使我们能够直接用C++代码编写上下文自由语法，并将其与$\mathrm{C}++$代码编译，以生成`parser` 。这样，就省去了从上下文自由语法到由外部工具在$\mathrm{C}++$中实现的`parser`的翻译步骤。Spirit中已经定义了几个小的解析器，帮助用户更容易地实现其解析器。

Spirit中的解析可以使用重载的解析函数之一来完成。其中一些重载函数在字符级工作，而另一些则在短语级工作，并采取和跳过 `parser` 。这个跳过 `parser` 在跳过空白和注释方面很有帮助。解析函数也被重载到它的输入。有一些重载像STL算法一样接受第一个和最后一个操作符，还有一些接受空结尾的字符串。在这项工作中，使用了带有首尾迭代器的短语级解析函数作为输入。

~~~C++
template <typename IteratorT, typename ParserT, typename SkipT>
parse_info<IteratorT> parse(
    IteratorT const &first,
    IteratorT const &last,
    parser<ParserT> const &p, // Grammar rules
    parser<SkipT> const &skip // Skip rules
);
~~~



现在让我们开始创建一个`parser`用于读取我们的节点语句的例子。这有助于看到如何使用Spirit生成一个简单的`parser`。下面是要解析的语句。
~~~C++
// a node statement:
// Node(Index ,X,Y,Z);
Node(1, 0.02, 1.00 , 0.00);
~~~


 首先，我们需要使用Spirit语法组件来定义我们的语法。[表9.4] 显示了Spirit中的有效运算符。这些运算符被定义为尽可能地接近其对应的正则表达式运算符，并尊重C++语言的限制。所以理解 `regular expressions` 也有助于使用 Spirit。



将这些运算符与一些预定义的Spirit解析器一起使用，可以创建必要的语法来读取节点语句。

~~~C++
(
str_p("Node")
>> ’(’
>> uint_p
>> ’,’
>> real_p
>> ’,’
>> real_p
>> ’,’
>> real_p
>> ’)’
)

~~~



`str_p`是一个预定义的`parser`，它用给定的字符串（即 "节点"）匹配输入。uint_p是另一个`parser`，它与输入的无符号整数相匹配，用于读取`Node` 的索引。 `real_p` 用来读取坐标。它与输入的一个实数相匹配。最后，任何字符都与它本身相匹配。这个语法可以用来解析语句，但当输入与每个部分相匹配时，仍然没有任何动作可做。所以我们必须给它添加一些动作。

~~~C++
(
    str_p("Node") 
    >> ’(’ >> uint_p[assign_a(node.Id())] 
    >> ’,’ 
    >> real_p[assign_a(node.X())][assign_a(node.X0())] 
    >> ’,’ 
    >> real_p[assign_a(node.Y())][assign_a(node.Y0())] 
    >> ’,’ 
    >> real_p[assign_a(node.Z())][assign_a(node.Z0())] 
    >> ’)’ 
)

~~~



这里assign_a是一个动作，它检索由`parser`读取的值并将其分配给它的参数。同样可以看出，当一条规则有多个动作需要执行时，动作可以层叠进行。为了处理注释和空白，我们需要一个跳过 `parser` 。这个`parser`可以简单地写成下面的样子。

~~~C++
(space_p | comment_p("//") | comment_p("/*", "*/"))
~~~


`space_p` 是Spirit中已经定义的一个`parser`，它与任何空白字符匹配。在spirit中还有一个预定义的`parser`，用于处理注释模式。 `comment_p` 只有一个参数，可以匹配从给定的字符串开始到行尾的序列，就像C++注释一样。 `comment_p` 带有两个参数，与从第一个参数开始到第二个参数结束的序列相匹配，就像C语言注释。

现在让我们把所有东西放在一起，创建我们的`ReadNode`方法。

~~~C++
void ReadNode(NodeType& rNode ,
              IteratorType First ,
              IteratorType Last)
{
    using namespace boost :: spirit;
    parse(First , Last ,
        // Begin grammar
        (
            str_p("Node")
            >> ’(’
            >> uint_p[assign_a(rNode.Id ())]
            >> ’,’
            >> real_p[assign_a(rNode.X())][ assign_a(rNode.X0 ())]
            >> ’,’
            >> real_p[assign_a(rNode.Y())][ assign_a(rNode.Y0 ())]
            >> ’,’
            >> real_p[assign_a(rNode.Z())][ assign_a(rNode.Z0 ())]
            >> ’)’
        )
        // End grammar
        ,
        // Skip grammar
        (space_p | comment_p("//") | comment_p("/*", "*/")));
}

~~~







这就是它! 这个函数可以解析由迭代器 `First` 和 `Last` 给出的任何字符序列。但是我们可能需要解析一个输入数据文件。这也可以通过稍微修改上述函数来实现。Spirit提供了一个文件迭代器，可以用正常的迭代器的方式来使用。这里是`ReadNode`的新版本，它能够从一个输入文件中读取`Node`。

~~~C++
void ReadNode(NodeType& rNode ,
std:: string Filename)
{
using namespace boost :: spirit;
FileIterator first(Filename );
FileIterator last= First.make_end ();

parse(first , last ,
        // Begin grammar
        (
            str_p("Node")
            >> ’(’
            >> uint_p[assign_a(rNode.Id ())]
            >> ’,’
            >> real_p[assign_a(rNode.X())][ assign_a(rNode.X0 ())]
            >> ’,’
            >> real_p[assign_a(rNode.Y())][ assign_a(rNode.Y0 ())]
            >> ’,’
            >> real_p[assign_a(rNode.Z())][ assign_a(rNode.Z0 ())]
            >> ’)’
        )
        // End grammar
        ,
        // Skip grammar
    (space_p | comment_p("//") | comment_p("/*", "*/")));
}

~~~



在这个IO中，我们也想解析新的组件，例如新的变量，`Elements`，或`Conditions`，而不用每次都改变和更新`parser`。Spirit提供了一个符号类，很适合我们的目的。这个类是一个与符号表相关的`parser`。这个表可以用不同的符号进行初始化，`parser`与存储在其表中的任何符号匹配。因此，在一个符号类中添加`KratosComponents`元素，提供一个`parser`来匹配这些组件。

~~~C++
template <class TComponentType>
class ComponentParser : public symbols<reference_wrapper<TComponentType const>>
{
public:
    ComponentParser()
    {
        typedef KratosComponents<TComponentType> ComponentsType;
        typedef ComponentsType ::ComponentsContainerType ContainerType;
        typedef ContainerType ::const_iterator iterator_type;
        iterator_type i_component;
        for (i_component = ComponentsType ::GetComponents().begin();
             i_component != ComponentsType ::GetComponents().end();
             i_component++)
            add(i_component->first.c_str(), i_component->second);
    }
};
~~~


`ComponentParser`派生自符号类，并在其构建时自动添加所有`TComponentsType`组件到符号表中。这使得它成为一个`parser`，与存储在`KratosComponents list`中的该类型的任何组件相匹配。

最后，所有这些概念和工具一起被用来创建一个灵活和可扩展的IO，用于Kratos 。

\subsection{Using Python as Interpreter} 

正如我们之前提到的，在Kratos的演变过程中，为IO部分实现完整的解释器的策略改变为使用一个现有的解释器。这种开发策略的改变，最终导致了使用Python解释器。

\subsubsection{Why another interpreter} 

在决定在IO功能中包括什么的时候，选择了一个IO的过程作为引入新算法而不改变代码和重新编译的一种方式。为了能够在实践中做到这一点，IO必须提供一个完整的`set`的语言流控制命令。

实现一个解释器是一项艰苦的工作。从管理的角度来看，它给项目成本和时间带来了巨大的开销。维护它更是难上加难，并导致代码成本的更多开销。另外，实现一个解释器需要不同概念的知识，如语法符号，一些工具和库的使用以及编译器的实现技术。有限元程序员通常不熟悉这些概念，在很多情况下甚至不喜欢处理这些问题。所以在开发有限元程序时，由于这种经验的缺乏，不容易与他人分享解释器的实现和维护。在实践中，这可能导致更长的实施时间和额外的成本。

在Kratos的实施过程中，所有这些事实都影响了解释器的实施，使其开发变得越来越困难，以至于成为项目的瓶颈之一。因此，我们改变了策略，不再编写解释器，而是寻找一个现有的解释器。将Kratos与Python捆绑在一起，使得它非常灵活和可扩展。新的算法可以使用现有的Kratos工具和方法来实现，甚至不用重新编译代码。领域之间的交互和规划不同的交错策略来解决`coupled problem`也可以在这个层面上轻松执行，而无需改变Kratos或其应用程序。此外，它还可以作为一个小型实验室，用于测试新的算法和公式，然后再将其编入 Kratos 。增加的功能`list`是无限的，许多复杂的任务都可以用这个界面轻松完成。

\subsubsection{Why Python} 

在大量可用的脚本语言中选择一个脚本语言并不是一件容易的事。每种不同的语言都有它自己的优势和劣势，在某些情况下是很有用的，但却忽略了一些目标。因此，我们的目的是首先了解需求，然后找到一种能更自然地适应这些任务的语言。

在这种情况下，有不同的功能需要语言的支持。

- 首先，所有要使用的解释器都必须有一个与$\mathrm{C}++$或至少与$\mathrm{C}$的接口。选择一个不能与我们的代码绑定的解释器真的没有意义。

- 可移植性是另一个需要考虑的基本点。 Kratos是用标准的C++编写的，并且还使用了可移植的库来提高其可移植性。所以解释器必须继续这个想法。

- 面向对象是一个重要的特征。虽然高水平的命令可以用传统的语言来表达，但是面向对象的语言可以更好地表达内部的类和数据结构。

- 语言的语法和它的可读性是另一个重要的特征。这取决于个人的品味，但自我描述的语法被认为比高度符号化的语法更清晰。

- 易学性也被认为是一个重要点。复杂而难学的语言会减少想要使用这些IO功能的用户数量。

- 灵活性和可扩展性是需要考虑的其他特征。为了增加有限元数据和容器，增加新的数据类型并通过接口使用它们是至关重要的。

- 由于存在新的未解决的问题的风险，在发展意义上活跃的语言将优于旧的但不活跃的语言。

- 最后，一种流行的语言是最好的，因为更大的程序员社区对处理日常问题和疑问有很大帮助。

从长长的`list`语言中，首先选择了一些比较流行的语言来进行验证。

- `Lisp`是广泛使用的脚本语言之一。然而，它充满小括号的语法和其他功能的缺乏导致它很快被我们的`list`所淘汰。这里有一个 `Lisp` 中的方形发生器样本。

~~~Lisp
(defun print-squares (upto)
    ( loop for i from 1 to upto
        do (format t "~A^2 = ~A~%" i (* i i))))
~~~



- `Perl`（实用提取和报告语言）是一种成熟的语言，具有快速解释器。此外，它还可以用其内置的`regular expressions`来解释数据文件，这对我们的案例来说是另一个附加价值。 `Perl`有一个模块化的结构，后来加入了一些面向对象的功能，并且是单线程，不支持多线程。下面是 `Perl` 中的打印方块代码。
~~~Perl
for($i = 1 ; $i <= 10 ; ++$i) {
    print $i*$i , ’ ’;
}
~~~


- `Ruby`是一个年轻但有吸引力的完全面向对象的语言，包括`Perl`的许多特性，有一个直观的语法。它还支持解释层面的多线程，这使得它也可以在单线程操作系统中使用。
~~~Ruby
for i in 1..10 do
    print i*i, " "
end
~~~


- `Tcl` 是一种广泛使用的脚本语言。它是一种模块化的语言，没有面向对象的特性，具有可移植性，可以很容易地与C语言集成。下面是 `Tcl` 中的打印方块例子。
~~~Tcl
set i 1
while {$i <= 10} {puts [expr $i*$i ];set i [expr $i +1]}
~~~

- Python是一种通用的脚本语言，具有面向对象的特性，语法非常简单和直观。它还支持多线程。下面是 Python 中的打印方块例子。
~~~Python
for i in range (1 ,11): print i*i,
~~~



由于一些细节和我们的背景，最终选择了Python。 `Tcl`是一个很好的选择，因为它已经在GiD中使用，并且有大量的经验。然而，由于缺乏面向对象的功能，所以没有选择。 `Perl`也因其成熟度和性能而被考虑，但它同样不如Python面向对象，并且不支持多线程。 `Ruby`在当时对我们来说是全新的，也被认为是某种程度上的年轻。

选择Python还有其他一些原因。现有的设计良好的Python界面是被选中的一个重要原因。此外，Python在有限元应用中也有一些实际用途 [^10][^11] 。

\subsubsection{Binding to Python} 

Boost Python 库用于绑定 Kratos 到 Python 。这个库提供了一个简单而强大的方法来连接 $\mathrm{C}++$ 代码和 Python 。

现在让我们创建一个 `Node` 到 Python 的接口，并把它作为使用这个库的逐步介绍。第一步是介绍 `Node` 类本身。下面的代码介绍了 `Node <3>` 类作为 `Node` 中的标识符。

~~~C++
typedef Node <3> NodeType;

class_ <NodeType , NodeType :: Pointer >("Node");
~~~



`class_` 模板将一个类引入到 Python 。第一个模板参数是要导入的类。第二个是Python中对这个类型的对象的处理程序。在这种情况下是 `Node` <>: 。指针，它是一个引用计数的指针。Boost Python 库可以正确处理这种类型的指针，并将其正确地绑定到 Python 的内存管理器中。构造函数的参数是 Python 中该类的代表名称。 Python没有模板，所以一个模板的不同实例可以暴露给Python而不是模板本身。例如，这里暴露了`Node<3>`的实例。

在Kratos中，`Node<>`来自`Point<>`和`IndexedObject` 。我们可以通过在其定义中引入`Node`的基数来保持这种层次结构。

~~~C++
typedef Node <3> NodeType;

class_ <NodeType , NodeType :: Pointer ,
    , bases <NodeType :: BaseType , IndexedObject > >("Node");
~~~



通过引入它的基础，`Node` 自动继承了 `Point<>` 和 `IndexedObject` 的所有导入方法。这个类现在已经暴露，可以通过它的默认构造函数来构造。但我们更喜欢通过它的id和坐标来构造它。`init`提供了一种引入构造函数的方法，可以作为另一个参数传递给 `class_` constructor。
~~~C++
typedef Node<3> NodeType;
class_<NodeType, NodeType ::Pointer,
       , bases<NodeType ::BaseType, IndexedObject>>("Node",
                                                    init<int, double, double, double>());
~~~



另一个构造函数可以使用 `class_` 的def方法添加。例如，一个由id和point组成的构造函数可以通过以下方式暴露。

~~~C++
typedef Node<3> NodeType;
class_<NodeType, NodeType ::Pointer,
       , bases<NodeType ::BaseType, IndexedObject>>("Node",
                                                    init<int, double, double, double>())
    .def(init<int, const Point<3> &>());
~~~



def使用了一个访问者模式，这使得它可以扩展到新的概念。这里init使用这种模式来暴露一个新的构造函数。现在我们可以在 Python 中创建 `Node` 并从 `Point<3>` 和 Indexed0bject 中调用其继承方法，但我们还不能打印它。为了使 `Node` 可以打印，必须添加另一条语句。


~~~C++
typedef Node<3> NodeType;
class_<NodeType, NodeType ::Pointer,
       , bases<NodeType ::BaseType, IndexedObject>>("Node",
                                                    init<int, double, double, double>())
    .def(init<int, const Point<3> &>())
    .def(self_ns ::str(self));
~~~


这个语句使用已经为 `Node` 定义的 << 操作符来打印它的信息。 def 也可以用来定义成员函数。例如 `Node` .  `IsFixed(Variable` )可以被导入到Python，如下所示。
~~~C++
typedef Node<3> NodeType;
class_<NodeType, NodeType ::Pointer,
       , bases<NodeType ::BaseType, IndexedObject>>("Node",
                                                    init<int, double, double, double>())
    .def(init<int, const Point<3> &>())
    .def("IsFixed", &NodeType ::IsFixed)
    .def(self_ns ::str(self));

~~~


要将Kratos作为一个模块导入，需要以下语句。

~~~C++
#include <boost/python.hpp >
BOOST_PYTHON_MODULE(Kratos) {
        // Module components will be defined here.
}
~~~

添加`IndexedObject`、`Point<3>`和`Node<3>`接口的结果就是第一个Kratos模块。

~~~C++
#include <boost/python.hpp >
BOOST_PYTHON_MODULE(Kratos)
{
    AddPointsToPython();
    class_<NodeType, NodeType ::Pointer,
           , bases<NodeType ::BaseType, IndexedObject>>("Node",
                                                        init<int, double, double, double>())
        .def(init<int, const Point<3> &>())
        .def("IsFixed", &NodeType ::IsFixed)
        .def(self_ns ::str(self));
}

~~~


`Point`接口被放在另一个函数中，只是在这里调用。这使得代码更容易阅读，同时也减少了编译这个文件所需的内存。

编译包括将Boost Python库本身和上面的代码编译为动态链接库，然后我们必须将它们放到Python动态链接库文件夹中。

在Python中，必须导入这个模块才能使用它的所有组件，或者只导入需要的组件。下面是一个在 Python 中使用该模块的例子。

~~~Python
from Kratos import *

#   Constructing a node:
node = Node(1, 2.00, 10.00, 0.00)

#   Using X property inherited from Point:
node.X = 4.5

#   Calling exposed IsFixed method
    if (node.IsFix(TEMPERATURE)) :
    # Using << operator of Node
    print node

~~~


这个简单的例子只是一个介绍。BoostPython库中的许多其他概念对于实现一个真正的接口是必要的。调用策略、虚拟和重载方法、异常处理等等都是这些概念的例子，在库的文档中都有描述。





\section{Validation Examples} 

本章介绍了一些使用Kratos开发的应用程序，以测试该框架所需的灵活性和可扩展性。

\subsection{Incompressible Fluid Solver} 

在这个例子中，描述了一个用Kratos实现的不可压缩流体应用。它显示了Kratos有效处理标准有限元公式的能力，同时实现了与单用途代码相当的性能。

\subsubsection{Methodology Used} 

一个`Arbitrary Lagrangian Eulerian`（ALE）公式被用来解决流体问题 [^32] 。解算器基于分数步长法 [^26] ，使用等阶压力-速度元素，通过正交子尺度（OSS）稳定 [^27] 。该应用采用了基于元素的方法，并对单数元素进行了优化。

\subsubsection{Implementation in Kratos} 

所选择的分步法包括四个求解步骤，其中第一个步骤涉及一个非线性循环，用于解决对流项的非线性问题，而其余的步骤是线性的，第三个步骤涉及投影项的明确计算。

这种方法可以通过创建一个新的`SolvingStrategy`结合现有的不同步骤来有效实施。该策略在$\mathrm{C}++$中是硬编码的，然而其实现是这样的：所有不同的步骤都可以单独解决。通过这种方式可以定义一个灵活的Python接口，允许与求解器直接交互。有了这个接口，算法的不同部分可以在性能损失最小的情况下重复使用现有策略。这种灵活性为在现有策略的基础上定义新的流体结构相互作用耦合策略提供了主要优势。

对于目前的应用，我们创建了一个组合的`Strategy`来处理求解过程。下面的代码显示了这个`Strategy`的求解方法，它调用其他方法来实现不同的步骤。


~~~C++
double Solve()
{
    // assign the correct fractional step coefficients
    InitializeFractionalStep(this->m_step, this->mtime_order);
    double Dp_norm;
    // predicting the velocity
    PredictVelocity(this->m_step, this->mprediction_order);
    // initialize projections at the first steps
    InitializeProjections(this->m_step);
    // Assign Velocity To Fract Step Velocity and Node Area to Zero
    AssignInitialStepValues();

    if (this->m_step <= this->mtime_order)
        Dp_norm = IterativeSolve();
    else
    {
        if (this->mpredictor_corrector == false) // standard fractional step
            Dp_norm = FracStepSolution();
        else // iterative solution
            Dp_norm = IterativeSolve();
    }
    if (this->mReformDofAtEachIteration == true)
        this->Clear();
    this->m_step += 1;
    this->mOldDt = BaseType ::GetModelPart().GetProcessInfo()[DELTA_TIME];
    return Dp_norm;
}
double FracStepSolution()
{
    // setting the fractional velocity to the value of the velocity
    AssignInitialStepValues();
    // solve first step for fractional step velocities
    this->SolveStep1(this->mvelocity_toll, this->mMaxVelIterations);
    // solve for pressures (and recalculate the nodal area)
    double Dp_norm = this->SolveStep2();
    this->ActOnLonelyNodes();
    // calculate projection terms
    this->SolveStep3();
    // correct velocities
    this->SolveStep4();

    return Dp_norm;
}

~~~




这个策略可以导出到Python而不损失性能，通过提供对上述策略类中实现每个步骤的方法的访问。我们可以在Python中写出一个等价的解决步骤，如下:

~~~Python

def SolutionStep1(self ):
    normDx = Array3 (); normDx [0] = 0.00; normDx [1] = 0.00; normDx [2] = 0.00;
    is_converged = False
    iteration = 0

    while( is_converged == False and iteration < self.max_vel_its ):
    (self.solver ). FractionalVelocityIteration(normDx );
        is_converged = (self.solver ). ConvergenceCheck(normDx ,self.vel_toll );
    print iteration ,normDx
        iteration = iteration + 1

def Solve(self ):
    if(self.ReformDofAtEachIteration == True ):
        (self.neighbour_search ). Execute ()

    (self.solver ). InitializeFractionalStep(self.step , self.time_order );
    (self.solver ). InitializeProjections(self.step );
    (self.solver ). AssignInitialStepValues ();

    self.SolutionStep1 ()

    (self.solver ). SolveStep2 ();
    (self.solver ). ActOnLonelyNodes ();
    (self.solver ). SolveStep3 ();
    (self.solver ). SolveStep4 ();

    self.step = self.step + 1

    if( self.ReformDofAtEachIteration == True ):
        (self.solver ). Clear ()


~~~


可以看出，Python的代码是不言自明的，而且很简单。提供这个接口还有一个很大的好处，就是允许用户定制全局算法，而不用访问 Kratos 中的内部实现。

为了实现元素公式，必须创建一个新的`Element`。`Element`应该为每个解决步骤提供不同的贡献。这是通过将当前的小数步数作为`Process`信息的变量传递给`Element`的计算方法来实现。有趣的是，这可以在不修改标准元素界面的情况下完成。这就是界面的通用性有助于整合新类型的公式的情况之一。下面的代码显示了这个计算方法的结构 `Element` 。

~~~C++
void Fluid3D ::CalculateLocalSystem(MatrixType &rLeftHandSideMatrix,
                                    VectorType &rRightHandSideVector,
                                    ProcessInfo &rCurrentProcessInfo)
{
    KRATOS_TRY
    int FractionalStepNumber = rCurrentProcessInfo[FRACTIONAL_STEP];
    if (FractionalStepNumber <= 3)
    {
        int ComponentIndex = FractionalStepNumber - 1;
        Stage1(rLeftHandSideMatrix,
               rRightHandSideVector,
               rCurrentProcessInfo,
               ComponentIndex);
    }
    else if (FractionalStepNumber == 4)
    {
        Stage2(rLeftHandSideMatrix,
               rRightHandSideVector,
               rCurrentProcessInfo);
    }
    KRATOS_CATCH("")
}

~~~





其中Stage1和Stage2是私有方法。

\subsubsection{Benchmark} 

Kratos是一个通用的代码。因此，预计它的性能会比为单一目的优化的代码低。一个良好的优化实现可以将性能开销减少到Kratos所引入的数量。我们努力优化上述的实现，因此将其性能与现有的流体求解器进行比较，以估计由Kratos引入的性能开销的顺序是很有意义的。

像往常一样，要做一个好的基准是不容易的，因为每个程序实现的公式都略有不同。尽管如此，还是可以与UPC的内部程序Zephyr和位于华盛顿特区的乔治华盛顿大学计算物理和流体动力学实验室（LCPFD）开发的高度优化的流体求解器`FEFLO`进行比较。对于第一种情况，配方是完全相同的，只是在实现上有微小的差别。第二个求解器是一个基于边缘的配方，唯一可能的比较是与预测器修正方案。

基准表示在雷诺数$R e=190$下对一个三维圆柱体的分析。图[10.1]显示了使用的模型。圆柱体壁上使用了无滑移边界条件，而其他地方则使用了滑移条件。流入速度为调至 $1 \mathrm{~m} / \mathrm{s}$ 。由R.Löhner教授提供的网格具有解决边界层的功能，包含30000个节点和$108 k$元素。图[10.2]显示了用于本试验的网格。计算的数值是圆柱体的升力和阻力历史。

图[10.3] 和 [10.4] 显示了由 Kratos 计算的压力和速度。从图[10.5]和[10.6]中可以看出，结果显示与`FEFLO`计算的值在峰值和脱落频率方面都非常一致。

时间上的结果也很有趣。 `FEFLO`似乎$50 \%$比Kratos快。考虑到`FEFLO`具有高度优化的基于边缘的数据结构，而Kratos是纯粹的基于元素的，这被认为是一个好的结果。


另一方面，Zephyr的特点是基于元素的表述，并实现了相同的分数步骤。主要区别在于投影项的处理和Zephyr中使用四个积分点来计算元素贡献。结果显示，Kratos的速度约为$100 \%$，求解时间约为Zephyr求解时间的一半。

\subsection{Fluid-Structure Interaction} 

耦合问题可以在Kratos中自然实现，利用Python的接口。流体求解器和结构求解器可以单独实现，并使用该接口耦合，没有任何问题。解决流体结构相互作用（FSI）问题所需的第一个动作是加载所涉及的不同应用程序。下面的Python代码显示这一步骤：

~~~Python
#including kratos path
kratos_libs_path = ’kratos/libs/’
kratos_applications_path = ’kratos/applications/’
import sys
sys.path.append(kratos_libs_path)
sys.path.append( kratos_applications_path)

# importing Kratos main library
from Kratos import *
kernel = Kernel () #defining kernel

#importing applications
import applications_interface
applications_interface.Import_ALEApplication = True
applications_interface.Import_IncompressibleFluidApplication = True
applications_interface . Import_StructuralApplication = True
applications_interface.Import_FSIApplication = True
applications_interface . ImportApplications (kernel , kratos_applications_path)

~~~


然后一个非常简单的显式耦合程序可以表示为:

~~~Python
class ExplicitCoupling:
def Solve(self ):
    # solve the structure (prediction)
    (self.structural_solver ). Solve ()
    ## map displacements to the structure
    (self.mapper ). StructureToFluid_VectorMap     (DISPLACEMENT ,DISPLACEMENT)
    ## move the mesh
    (self.mesh_solver ). Solve ()
    ## set the fluid velocity at the interface to
    ## be equal to the corresponding mesh velocity
    self.CopyVectorVar(MESH_VELOCITY ,VELOCITY ,self.      interface_fluid_nodes );
    ## solve the fluid
    (self.fluid_solver ). Solve ()
    ## map displacements to the structure
    (self.mapper ). FluidToStructure_ScalarMap(PRESSURE ,      POSITIVE_FACE_PRESSURE )
    # solve the structure (correction)
    (self.structural_solver ). Solve ()
~~~



当然，许多不同的耦合方案可以在不对单一场求解器进行修改的情况下实现。图中给出了一个流体与结构相互作用的例子 $10.7$ 。




\subsection{Particle Finite Element Method} 

粒子有限`Element`法（`PFEM`）[^76][^51][^50][^49]是一种用于解决任意变化域上的流体问题的方法。其基本概念是，每个粒子以滞后的方式被跟踪，网格在每个时间步长中被重新生成。

所面临的主要计算挑战是网格的有效再生和所有元素贡献的优化重新计算。通过与外部网格生成库的连接和使用优化的Kratos流体求解器，可以获得良好的性能。解算顺序由Python接口控制。这里给出了Python脚本的一部分。

~~~Python
def Solve(self ,time ,gid_io ):
    self.PredictionStep(time)
    self.FluidSolver.Solve ()

def PredictionStep(self ,time ):
    domain_size = self.domain_size
    # performing a first order prediction of the fluid displacement
    (self.PfemUtils ). Predict(self.model_part)
    self.LagrangianCorrection ()
    (self.MeshMover ). Execute ();
    (self.PfemUtils ). MoveLonelyNodes(self.model_part)
    (self.MeshMover ). Execute ();
    ## ensure that no node gets too close to the walls
    (self.ActOnWalls ). Execute ();
    ## move the mesh
    (self.MeshMover ). Execute ();

    ## smooth the final position of the nodes to
    ## homogenize the mesh distribution
    (self.CoordinateSmoother ). Execute ();
    ## move the mesh
    (self.MeshMover ). Execute ();
    # regenerate the mesh
    self.Remesh ()
~~~


这个例子显示了在实现新的算法时如何重复使用以前实现的流体求解器。这种可重用性允许快速开发新的公式，这些公式可以通过解决大规模的实际问题进行测试。图[10.8]显示了用`PFEM`中实现的Kratos[^57]中的应用所做的破坝模拟。

\subsection{Thermal Inverse Problem} 

逆向问题在科学和工程的许多领域都有。它们可以被描述为与直接问题相反。在直接问题中，原因已经给出，而结果已经确定。在逆向问题中，效果已经给出，而原因必须被估计出来 [^52] 。有两种主要类型的反问题：输入估计问题，其中系统属性和输出是已知的，输入需要估计；属性估计问题，其中系统输入和输出是已知的，属性需要估计 [^52] 。

在数学上，逆问题属于更普遍的变分问题。变分问题的目的是找到一个函数，使指定函数的值最小。所谓函数，我们指的是一种对应关系，它为属于某个类别的每个函数分配一个数字。此外，逆问题可能是不完善的，在这种情况下，解决方案可能不符合存在性、唯一性或稳定性要求。

虽然一些简单的逆问题可以用分析法解决，但一般问题的唯一实用技术是用直接法近似解决。所谓直接方法的基本思想是将变分问题视为某些函数优化问题在许多维度上的极限问题。不幸的是，由于其变异性和ill-posed问题性，逆问题很难解决。



 `Neural networks` 是人工智能的主要领域之一 [^47] 。有许多不同类型的神经网络，其中 `multilayer perceptron` 是重要的一种 [^94] 。 `Neural networks` 为解决一般的变分问题以及由此产生的反问题提供了一种直接的方法 [^30] 。

在这个例子中，神经网络被用来解决热逆问题。为了解决这个问题，我们需要解决传热方程。在CIMNE开发的`Flood`库[^60]是一个用$\mathrm{C}++$编写的开源神经网络库，用于创建解决此问题所需的神经网络。虽然这个库不包括用于解决偏微分方程的工具，但它使用Kratos及其热学应用来解决热学问题。这个例子验证了Kratos作为另一个项目的库的可整定性。它还证明了它的鲁棒性，因为这个算法运行Kratos对同一个模型进行了多次分析。在这种情况下，任何小问题（例如，在内存管理中）都可能导致执行错误。

\subsubsection{Methodology} 

使用`Neural networks`解决变量问题的一般方法包括三个步骤[?]。

- 定义函数空间。这里的解要用一个`multilayer perceptron`来表示。

- 变量问题的表述。为了它们的效果，必须定义一个性能函数$F(y(x, a))$。为了评估该函数，我们需要解决一个偏微分方程，这是在 Kratos 内使用有限元法完成的。

- 缩小的函数优化问题的解决 $\mathrm{f}(\mathrm{a})$ 。这是由训练算法实现的。训练算法将多次评估性能函数$f(a)$。

图[10.10]显示了这三个步骤。

这个算法提供了一个例子，说明如何将Kratos嵌入到一个优化应用中，在这个应用中，为了实现解决方案，必须采取不同的有限元分析步骤。 Kratos已被嵌入到`Flood`库内作为其求解引擎，以计算偏微分方程的解。

在这里，上述方法被应用于解决两个不同的热逆问题。

\subsubsection{Implementation} 

`Flood`库使用Kratos来多次解决具有不同属性和边界条件的热问题。为了做到这一点，它必须访问 Kratos 的内部数据，并改变分配给不同 `Nodes` 的边界条件。这是在没有任何文件接口的情况下完成的，这将大大降低性能。第一部分是使用 Kratos 进行直接求解的接口。下面的代码显示了这个接口的主要部分。



~~~C++
// Read mesh
Kratos ::GidIO gidIO("thermal_problem");
gidIO >> mesh;
// Set properties
mesh.GetProperties(1)[DENSITY] = density;
mesh.GetProperties(1)[SPECIFIC_HEAT_RATIO] = specificHeat;
mesh.GetProperties(1)[THERMAL_CONDUCTIVITY] = thermalConductivity;
// Assign initial temperature
for (MeshType ::NodeIterator i_node = mesh.NodesBegin();
     i_node != mesh.NodesEnd(); i_node++)
    if (!(i_node->IsFixed(TEMPERATURE)))
        i_node->GetSolutionStepValue(TEMPERATURE) = initialTemperature;
// Creating solver
// ...
// Main loop
for (int i = 1; i < numberOfTimeSteps; i++)
{
    // Obtain time
    time[i] = time[i - 1] + deltaTime;
    // Obtain boundary temperature
    // Gaussian function

    double mu = 0.5;
    double sigma = 0.05;
    double numerator = exp(-pow(time[i] - mu, 2) / (2.0 * pow(sigma, 2)));
    double denominator = 8 * sigma * sqrt(2.0 * pi);
    boundaryTemperature[i] = numerator / denominator;
    // Assign boundary temperature
    for (MeshType ::NodeIterator i_node = mesh.NodesBegin();
         i_node != mesh.NodesEnd(); i_node++)
        if (i_node->IsFixed(TEMPERATURE))
            i_node->GetSolutionStepValue(TEMPERATURE) =
                boundaryTemperature[i];
    // Solving using thermal solver
    Solve();
    // Now updating the nodal temperature values
    // by result of solved equation system.
    Update();
    // Obtain node temperature
    nodeTemperature[i] =
        mesh.GetNode(nodeIndex).GetSolutionStepValue(TEMPERATURE);
    equation_system.ClearData();
}

~~~











初始化求解器的部分已被删除，以使示例代码更短，只保留了`Flood`用于与Kratos互动的部分。这段代码显示了Kratos为其他应用程序提供的灵活但清晰直观的界面，以便与它进行通信。 首先，应用程序将`Element`的属性改为其规定值。然后，它将所有`Nodes`的温度值改为某个初始值。之后，它试图使用不同的边界条件来解决，给`Nodes`指定固定的温度值。最后，它在一个特定的`Node`处获取温度。可以看出，有些步骤是直接在时间循环内进行的。这限制了我们使用通常的时间过程来解决问题。

反问题也需要类似的步骤，但以`Flood`库的性能函数的形式。在这种情况下，Kratos适应`Flood`的工作方法没有问题。

\subsubsection{The Boundary Temperature Estimation Problem} 

对于边界温度估计问题，考虑方域$\Omega=\{(x, y):|x| \leq 0.5,|y| \leq 0.5\}$中的热方程，边界$\Gamma=\{(x, y):|x|=0.5,|y|=0.5\}$ 。

\begin{equation}
\nabla^{2} u(x, y ; t)=\frac{\partial u(x, y ; t)}{\partial t} \quad \text { in } \quad \Omega
\end{equation}

为 $t \in[0,1]$ ，初始条件 $u(x, y ; 0)=0$ 在 $\Omega$ 。问题是根据对$\Gamma$和$t \in[0,1]$的正方形中心温度$u(0,0 ; t)$的测量来估计边界温度$y(t)$。

其中$P$是考虑的时间步数。对于这个问题，我们使用101个时间步骤。

解决这个问题的第一阶段是选择一个网络结构来表示边界温度 $y(t)$ 为 $t \in[0,1]$ 。这里使用了一个`multilayer perceptron`，有一个sigmoid隐藏层和一个线性输出层 [^47] 。这种神经网络是一类通用近似器 [^48] 。该网络必须有一个输入和一个输出神经元。我们猜测隐蔽层中的神经元数量好是六个。这个神经网络跨越了一个维度为$V$的参数化函数$y(t ; \underline{\alpha})$族，该维度为网络中自由参数的数量。

第二阶段是推导出一个性能函数，以制定变量问题。这就是在给定的边界温度下，广场中心的计算温度与广场中心的测量温度之间的平均平方误差。

\begin{equation}
F[y(t ; \underline{\alpha})]=\frac{1}{P} \sum_{i=1}^{P}\left(\hat{u}_{y(t ; \underline{\alpha})}\left(0,0 ; t_{i}\right)-u_{i}\left(0,0 ; t_{i}\right)\right)^{2}
\end{equation}

请注意，性能函数（10.2）的评估需要用数值方法来解决偏微分方程。 这里使用Kratos来解决这个问题。

然后，`multilayer perceptron`的边界温度估计问题可以被表述为：。

让$V$成为由所有函数$y(t ; \underline{\alpha})$ 组成的空间，该空间由一个`multilayer perceptron`跨越，该空间有1个输入，隐藏层有6个sigmoid神经元，1个线性输出神经元。$V$的维度是$19$ 。找到一个自由参数$\underline{\alpha}^{*} \in \mathbf{R}^{19}$的`vector`，解决一个函数$y^{*}\left(t ; \underline{\alpha}^{*}\right) \in V$，对于这个函数（10.2），定义在$V$上，采取一个最小值。

解决这个问题的第三个阶段是选择一个合适的训练算法。这里我们使用共轭梯度与Polak-Ribiere训练方向和Brent最佳训练率[^24]。布伦特方法的容忍度是调到 $10^{-6}$ 。用共轭梯度算法训练神经网络需要评估性能函数梯度 `vector` $\nabla f(\underline{\alpha})$ 。这是通过数值微分的方式进行的 [^24] 。特别是，我们使用对称中心差分法 [^24] ，其 $\epsilon$ 值为 $10^{-6}$ 。

在这个例子中，我们 `set` 训练算法在性能函数梯度 $\nabla f(\underline{\alpha})$ 的规范值低于 $10^{-6}$ 时停止。这意味着局部最小值的必要条件已经得到满足。神经网络以`vector`的自由参数初始化，这些参数是在[-1,1]区间内随机选择的。在训练过程中，性能函数不断下降，直到满足停止标准。[表10.1]显示了这个问题的训练结果。这里$N$表示历时数，$M$表示性能评估数，$F\left[y^{*}\left(t ; \underline{\alpha}^{*}\right)\right]$ 表示最终性能，$\nabla f\left(\underline{\alpha}^{*}\right)$表示最终性能函数梯度规范。

图[10.11]显示了这个问题的实际边界温度、神经网络估计的边界温度和广场中心的测量温度。



这里的解决方案很好，因为估计的边界温度与实际的边界温度相符。





\subsubsection{The Diffusion Coefficient Estimation Problem} 

对于扩散系数估计问题，考虑在边界为$\Omega=\{(x, y):|x| \leq 0.5,|y| \leq 0.5\}$的正方形域$\Gamma=\{(x, y):|x|=0.5,|y|=0.5\}$中不均匀介质的扩散方程。

\begin{equation}
\nabla(\kappa(x, y) \nabla u(x, y ; t))=\frac{\partial u(x, y ; t)}{\partial t} \quad \text { in } \Omega
\end{equation}

为$t \in[0,1]$ ，其中$\kappa(x, y)$称为扩散系数，边界条件$u(x, y ; t)=$ 在 $\Gamma$ 和 $t \in[0,1]$ 上为0，初始条件 $u(x, y ; 0)=1$ 在 $\Omega$ 。问题是估计 $\Omega$ 中的扩散系数 $\kappa(x, y)$ ，从 $\Omega$ 中的正方形 $u(x, y ; t)$ 和 $t \in[0,1]$ 上不同点的温度测量值中得出，其中 $P$ 和 $Q$ 分别为考虑的点和时间步数。对于这个问题，我们使用485个点和11个时间步骤。

$$
\begin{array}{ccccc}
t_{1} & u_{11}\left(x_{1}, y_{1} ; t_{1}\right) & u_{12}\left(x_{2}, y_{2} ; t_{1}\right) & \ldots & u_{1 Q}\left(x_{Q}, y_{Q} ; t_{1}\right) \\
t_{2} & u_{21}\left(x_{1}, y_{1} ; t_{2}\right) & u_{22}\left(x_{2}, y_{2} ; t_{2}\right) & \ldots & u_{2 Q}\left(x_{Q}, y_{Q} ; t_{2}\right) \\
\vdots & \vdots & \vdots & \ddots & \vdots \\
t_{P} & u_{P 1}\left(x_{1}, y_{1} ; t_{P}\right) & u_{P 2}\left(x_{2}, y_{2} ; t_{P}\right) & \ldots & u_{P Q}\left(x_{Q}, y_{Q} ; t_{P}\right)
\end{array}
$$



解决这个问题的第一阶段是选择一个网络结构来表示 $\Omega$ 中的扩散系数 $\kappa(x, y)$ 。这里使用了一个带有sigmoid隐藏层和线性输出层的`multilayer perceptron`。这个神经网络是一类通用近似器 [^48] 。该神经网络必须有两个输入和一个输出神经元。我们猜测隐蔽层中的神经元数量好是六个。这个神经网络被表示为一个 $2: 6: 1$ `multilayer perceptron` 。它跨越了一个维度为$s=25$的参数化函数$V$系列，这是网络中自由参数的数量。

第二阶段是为扩散系数估计问题推导出一个性能函数。这个问题的性能函数是给定扩散系数的计算温度和测量温度之间的平均平方误差。

\begin{equation}
E[\kappa(x, y ; \underline{\alpha})]=\frac{1}{P Q} \sum_{i=1}^{P}\left(\sum_{j=1}^{Q}\left(\hat{u}_{\kappa(x, y ; \underline{\alpha})}\left(x_{j}, y_{j} ; t_{i}\right)-u_{i j}\left(x_{j}, y_{j} ; t_{i}\right)\right)^{2}\right)
\end{equation}

那么，`multilayer perceptron`的扩散系数估计问题可以表述如下:

让$V$成为由所有函数$\kappa(x, y ; \underline{\alpha})$组成的空间，该空间由一个有2个输入的`multilayer perceptron`、隐藏层的6个sigmoid神经元和一个线性输出神经元跨越。$V$的维度是$25 .$ 找到一个`vector`的自由参数$\underline{\alpha}^{*} \in \mathbf{R}^{25}$，解决一个函数$\kappa^{*}\left(x, y ; \underline{\alpha}^{*}\right) \in V$，对于这个函数（10.4），定义在$V$，采取最小值。

性能函数（10.4）的评估需要一种偏微分方程积分的数值方法。这里我们选择有限`Element`方法[^104] 。对于这个问题，我们使用888个元素和485个节点的三角形网格。

第三阶段是选择一个合适的算法进行训练。这里我们使用共轭梯度与Polak-Ribiere训练方向和Brent最优训练率方法进行训练 [^24] 。Brent方法的容忍度是调到 $10^{-6}$ 。用共轭梯度算法训练神经网络需要评估性能函数梯度 `vector` $\nabla f(\underline{\alpha})$ [^24] 。这是以数值微分的方式进行的。特别是，我们使用对称中心差分法 [^24] 与 $\epsilon=10^{-6}$ 。

在这个例子中，我们训练算法在性能函数梯度的准则低于 $10^{-6}$ 时停止。这意味着局部最小值的必要条件已经得到满足。神经网络以`vector`的自由参数初始化，这些参数是在区间[-1,1]内随机选择的。[表10.1]显示了这个问题的训练结果。这里$N$表示历时数，$M$表示性能评估数，$F\left[\kappa^{*}\left(x, y ; \underline{\alpha}^{*}\right)\right]$表示最终性能，$\nabla f\left(\underline{\alpha}^{*}\right)$表示最终性能函数梯度规范。








进一步的应用和其他类别的问题可以在 [^73][^85][^86][^88][^87][^29] 中找到。





\section{Conclusions and Future Work} 

\subsection{Conclusions} 

 Kratos ，一个开发多学科程序的框架已经被设计和实现。它通过提供输入输出、数据结构、求解器、基本工具和标准算法，帮助开发者实现不同领域的分析应用。在这个框架中实现的应用程序可以使用任何主从策略或甚至同时求解来解决多学科问题。目前，在 Kratos 中实现了几个求解器（不可压缩的流体、结构、热和电磁）。这些应用程序的组合也被用来解决不同的多学科问题，特别是流体-结构相互作用和热-结构问题。

这个框架提供了处理多学科问题所需的高度的灵活性和通用性。不同领域的开发者可以根据他们的需要配置Kratos，而不需要改变用于与其他领域的耦合分析进行交流的标准接口。不同的应用如：粒子有限元法和显式可压缩流体在Kratos中实现，这有助于验证其处理不同算法的灵活性。最后，它的python接口在处理非标准算法方面提供了额外的灵活性。

提供了几个可重复使用的组件，以帮助开发人员更容易和更快地实现他们的应用程序。数据结构、IO、线性求解器、几何图形、正交工具和不同的策略是这些可重用组件的例子。使用这些组件不仅使应用开发更快，而且还能确保与其他解决多学科问题的工具兼容。

 Kratos在不同的实现层次上也是非常可扩展的。每个应用程序都可以将其变量、自由度、`Properties`、`Elements`、`Conditions`和解决算法添加到Kratos。其设计中使用的面向对象的结构和适当的模式使这些扩展变得容易，同时减少了修改的需要。通过实现不同的应用，从标准的有限元应用到使用Kratos及其应用的优化程序，可扩展性也在各个层面得到验证。

 `Last`但同样重要的是，Kratos的性能甚至可以与单用途程序相媲美，不同的基准在实践中显示了这一点。这使得Kratos成为解决工业多学科问题的实用工具。

\subsubsection{General Structure} 

为了最大限度地提高代码的可重用性和可扩展性，设计了一个面向对象的结构。这个结构是基于有限元方法的，许多对象被设计用来表示基本的有限元概念。通过这种方式，该结构对于具有有限元方法背景的开发人员来说变得容易理解。

在这个设计中，`Vector`，`Matrix`，和`Quadrature`代表了基本的数值概念。 `Node`, `Element`, `Condition`, 和`Dof`是直接以有限元的概念定义的。 `Model`, `Mesh`, 和`Properties`是来自于有限元建模中使用的实际方法，辅以`ModelPart`, 和`SpatialContainer`，用于更好地组织分析所需的所有数据。I0, `LinearSolver`, `Process`, 和 `Strategy`代表了有限元程序流程的不同步骤。最后，`Kernel`和应用程序被定义为库管理及其接口定义。

 Kratos在其设计中使用了多层方法，减少了程序的不同部分之间的依赖性。它有助于代码的维护，也有助于开发人员理解代码。这些层的定义方式是，每个用户必须在尽可能少的层中工作。这样一来，每个用户必须熟悉的代码量就会降到最低，不同类别的用户之间发生冲突的机会也会减少。每层所需的实施难度也是根据用户的工作知识来调整的。例如，有限元层只使用$\mathrm{C}++$编程的基本到一般的功能，但主要开发者层使用高级语言功能，以提供所需的性能。

\subsubsection{Basic Tools} 

在 Kratos 中，已经实现了不同的可重用工具来帮助开发者编写他们的应用程序。提供了几种几何图形和不同的正交方法，并对其性能进行了优化。它们的灵活设计和通用接口使它们适合于不同的应用。它们的优化性能使它们不仅适合于学术应用，而且也适合于实际的工业模拟。

一个可扩展的线性求解器结构已经被设计出来，不同的通用求解器也已经被实现。在这个设计中，求解器只封装了求解算法，所有对向量和矩阵的操作都被封装在空间类中。通过这种方式，求解器变得与数学容器的类型无关，可以用来解决完全不同类型的方程组，如对称、天际等。这种结构也允许高度优化的求解器（只针对一种类型的矩阵或向量）被毫无问题地实现。

\subsubsection{`Variable` Base Interface} 

设计并实现了一个新的变量基础接口。所有通过该接口传递的关于概念或变量的信息都被封装在`Variable`类中。有关变量的组成部分的信息也被封装在`VariableComponent`类中，这为该接口提供了额外的灵活性。这个接口在不同的抽象层次上使用，并被证明是非常清晰、灵活和可扩展的。

 `Variable`静态地提供了数据的类型，对象可以使用它来通过模板实现为给定的数据类型配置其操作。这种类型信息还可以防止在不能处理其数据类型的程序中使用变量。每个变量都有一个唯一的键，可以作为数据结构中的参考键。变量的名字是一个字符串，这有助于像IO这样的例程读取和写入它们而不需要额外的参数。最后，它提供了一个零值，可用于初始化数据，与通用算法的类型无关。除了这些信息之外，变量还提供了不同的方法来进行原始内存操作。这些方法是低级通用编程的优秀工具，特别是在编写异构容器时。

这个接口已经在 Kratos 的不同部分被成功使用。它的灵活性和可扩展性在实践中得到了证明，它对代码的可读性也有明显的贡献。这个接口在统一来自不同分析领域的不同概念方面发挥了巨大作用。

\subsubsection{Data Structure} 

新的异质容器已经被实现，以便在不做任何修改的情况下保存不同类型的数据。DataValueContainer可以用来存储任何类型的变量，甚至不需要明确定义它们的`list`。这个容器非常灵活，但使用搜索机制来检索给定变量的数据。VariablesListContainer只存储在其变量`list`中定义的变量，这些变量可以有任何类型，但它的优势在于其快速`indirection`的机制来查找变量数据。在Kratos中，这两个容器在性能或灵活性更重要的地方被交替使用。甚至能够存储邻居`Nodes`或`Elements`的`list`，显示了它们在实践中的灵活性。

在 Kratos 中已经开发了一个实体基础数据结构。这种方法在划分领域或创建和删除 `Nodes` 和 `Elements` 时提供了更多的自由，例如在自适应网格划分中。提供了几个抽象层次，以帮助用户以不同方式分组模型和数据信息。在Kratos中，`Model`包含整个模型，分为不同的ModelParts。每个模型部分可以有不同的Meshes，这些Meshes容纳了Kratos中完整的`set`的实体。这些对象有效地用于分离领域信息或将单个部分发送到一些进程。

\subsubsection{Finite `Element` Implementation} 

`Element` 和 `Condition` 类被设计为 Kratos 的扩展点。它们的通用接口提供了计算其本地组件所需的所有信息，同时也为将来处理新的论据提供了足够的灵活性。

一些程序和策略已经被开发出来以处理有限元编程中的标准程序。这些组件增加了代码的可重用性，减少了使用Kratos实现新的有限元应用所需的努力。

一些实验性的工作已经完成，使用更高层次的抽象来处理元素表达。通过这种方式，元素表达式可以用$\mathrm{C}++$来编写，但使用的元语言与数学符号非常相似，然后可以使用$\mathrm{C}++$编译器与其他代码一起编译。这些表达式已经成功地进行了测试，其性能与手工实现的代码相当。

最后，该表达式被设计用来处理节点、基于边缘、甚至是元素的表达式的不同实现形式。然而，由于开发人员缺乏兴趣，这些能力还没有被实现。

\subsubsection{Input-Output} 

为有限元程序开发了一个灵活和可扩展的IO模块。它可以非常容易地处理新的概念，而Kratos自动为其组件添加变量，IO使用这些组件作为其`list`的概念。因此，任何用Kratos构建的应用程序都可以使用IO来读写自己的概念，而无需对其进行任何修改。然而，需要更多的努力来扩展这个IO系统以处理新的数据类型。

这个IO是多格式的。它可以支持新的格式，只需添加一个新的 IO 派生类，而不需要改变 IO 的任何其他部分。例如，一个二进制格式IO可以使用这个功能添加。

还实现了一个解释器来处理 Kratos 数据文件。它的格式相对直观，与Python脚本相似。主要的解释任务交给了 Python 解释器。这个灵活的解释器及其面向对象的高级语言可以用来实现和执行使用 Kratos 的新算法。这样就省去了新的复杂解释器的实施和维护成本。

\subsection{Future Work} 

这项工作可以以不同的方式继续进行。 首先，对现有框架的所有不同的扩展可以增加它所提供的有用功能的数量。除了这些扩展，代码的并行化是下一个要执行的任务，以保证其在解决未来的大规模问题中取得成功。

\subsubsection{Extensions} 

这里有一个`list`对这项工作的扩展建议。

\subsubsection{Basic Tools} 

应该增加新的求解器和预处理器，以扩展 Kratos 的求解能力。同时，应该实现一种新型的用于非常小的方程组的线性求解器。它们可以用来有效地解决一些算法中出现的小方程，如补丁恢复法 [^104] 。

\subsubsection{`Variable` Base Interface} 

如前所述，在这个界面中，有一些与变量及其组成部分不相容有关的复杂问题。这也是一扇开放的大门，可以进一步改进。一个解决方案可以是从变量中导出`VariableComponent`，并使用一些索引机制来区分它们。这可以通过一些`traits`来完成，以避免在需要良好性能的情况下调用虚拟函数。这是一个需要仔细实现和测试的东西。

\subsubsection{Finite `Element` Implementation} 

创建新的流程和策略可以提高代码的可重用性，也可以提高 Kratos 的完整性。这也可以通过修改在不同应用中实现的流程和策略，并在Kratos中加入一个通用版本，可用于更广泛的`set`应用。

如8.5节所述，已经开发并测试了一个实验性的元素表达。这些部分需要更多的组件来可用于广泛的配方。实现缺失的组件并实际使用它们可以帮助Kratos中的有限元表达式的快速发展，同时，它可以用来优化新的表达式，甚至将它们自动转化为并行代码。配方是Kratos中另一个需要探索的部分。在Kratos中加入基于节点或边缘的公式，可以是在实践中完善其设计的一个好方法。

\subsubsection{Input-Output} 

序列化还没有实现，但被认为对问题加载和保存的自动化很有用。在Kratos中增加这一功能将有助于用户运行较长的问题，并随时暂停它们。使用一个外部库被认为是比实现它更好的解决方案。

支持二进制格式的输入可以大大减少数据的读取时间。Kratos的多格式功能减少了很多实现它的必要努力。

\subsubsection{Parallelization} 

除了前面提到的扩展，Kratos框架的并行化是未来要进行的主要任务。问题的规模越来越大，可用的并行计算机器越来越多（甚至在个人电脑领域），这强调了数字代码并行化的重要性。由于这个原因，应该投入大量的精力，对共享内存和分布式内存架构的Kratos进行并行化。

幸运的是，Kratos的几个方面在这个过程中变得有用。它的基于实体的数据结构使数据在处理器上的分布更加容易。另外，数据结构中的几层抽象将有助于分区任务，这也是在处理器上划分模型的需要。最后，`Strategy`被设计成只需很小的努力就能实现并行化。

\subsection{Acknowledgments} 

这项工作要献给Hengameh，她离开了她的家人、朋友，也离开了她的工作，和我一起来到了巴塞罗那。在这漫长的岁月中，她的支持和耐心是无价的。

我要感谢Eugenio Oñate教授的支持，他的支持使这个项目成为可能，还要感谢整个Kratos团队的贡献和宝贵的反馈，这对Kratos的发展起了决定性作用。

我还要特别感谢我的好朋友Riccardo Rossi博士，他作为一个非常好的工程师和程序员在开发Kratos时做出了巨大的努力。他帮助我克服了在这项工作中出现的一些困难，不仅有他的实际解决方案，还有他对这个项目的高度积极性和信念。

这项工作是为了纪念Francisco Javier Royo，他开始了这项工作，但他短暂的生命没有让他完成它...... 88 





![3.1](https://cdn.mathpix.com/snip/images/Vuy948tmSzAF01iae-29Rg1xWXXd-u80E5x6FEXRKE0.original.fullsize.png)
Figure 3.1: Three steps of Numerical analysis process.


![3.2](https://cdn.mathpix.com/snip/images/HqiUxi8uAuE6QefJY209KB2og9NhwoAQNj3pkDOtBRQ.original.fullsize.png)
Figure 3.2: The problem of finding $u$ over a domain $\Omega$.


![3.3](https://cdn.mathpix.com/snip/images/TZbrdDnDNvOaqHIbRSXRzh2jVdg2bxI5_pck2dQoehQ.original.fullsize.png)
Figure 3.3: A Thermal domain $\Omega$ with fixed temperature boundary $\Gamma_{\theta}$ and fixed flux boundary $\Gamma_{q}$ 



![3.4](https://cdn.mathpix.com/snip/images/l9O4MOXzJ0KJOGbCwbGuFiPolTfKc5zSGUKENfVLcVI.original.fullsize.png)
Figure 3.4: A regular domain discretized with a finite difference grid.



![3.5](https://cdn.mathpix.com/snip/images/fuQctc_TafzE3Vk8e5l8U4QxxaYekrEMRgEsUTxmy00.original.fullsize.png)
Figure 3.5: An arbitrary geometry and its finite difference discrete model.


![3.6](https://cdn.mathpix.com/snip/images/zuH2P2NcQ1UPmqzoNMYUMKUXW8SSsvJCBdF9vDyzmXg.original.fullsize.png)
Figure 3.6: An element $\Omega^{e}$ and its boundary $\Gamma^{e}$.

![3.7](https://cdn.mathpix.com/snip/images/3ICGd65Cv4xWJiNTd6BmSh5_6AQAz9kY4Pr1jHsi_oI.original.fullsize.png)
Figure 3.7: A domain and its `Dirichlet` and `Neumann ` boundaries.

![3.8](https://cdn.mathpix.com/snip/images/9wnWXqHW6J59k4oXpes8-56nRIoFAscieb6PvTPqZSs.original.fullsize.png)
Figure 3.8: A simple beam example with two elements and three nodes.

![3.9](https://cdn.mathpix.com/snip/images/Aesg3nz2LqX3t-kM8mKi9zpEMSqiFBJtgaXLh_QJcTU.original.fullsize.png)
Figure 3.9: A general multi-disciplinary problem with two subsystems. 

![3.10](https://cdn.mathpix.com/snip/images/q1BQ_Zi-t46b6sbGJ8_1InWZMuP5rHDoFNC_6YkbPLo.original.fullsize.png)
Figure 3.10: A weak coupled system where the subsystem $S_{2}$ depends on the solution of subsystem $S_{1}$

![3.11](https://cdn.mathpix.com/snip/images/SI2dCvzcMxUA0UZM4Myk6tmnYoAuL6bPchcU0avWZ5o.original.fullsize.png)
Figure 3.11: A strong coupled system where not only the subsystem $S_{2}$ depends on the solution of subsystem $S_{1}$ but also subsystem $S_{1}$ depends on $S_{2}$.

![3.12](https://cdn.mathpix.com/snip/images/ByeyQR2a_7vGfkMkkJKaWDQV6iaWhVTRyfWJvVtBMbA.original.fullsize.png)
Figure 3.12: A class I fluid-structure interaction problem. The interface, shown by the thick black line, is just at the boundary of the fluid and structure domains.

![3.13](https://cdn.mathpix.com/snip/images/SfGO6nDdWpBtVXEEbpdIJwUkyiO70WOsXt-KOSukN9o.original.fullsize.png)
Figure 3.13: A thermal-fluid interaction problem. Here the thermal domain and the fluid domain overlap in the heating pipe part. 

![3.14](https://cdn.mathpix.com/snip/images/CBw2ZLvwECkGT1mb3gmc7PY0GsMui_12LidmWikuaVI.original.fullsize.png)
Figure 3.14: Sequential solving of `one-way` coupled problems.

![3.15](https://cdn.mathpix.com/snip/images/Po5gr8vigHHK6yc-bgU0UWbGFGpSEXinDmwdv79Qads.original.fullsize.png)
Figure 3.15: Monolithic scheme for solving multi-disciplinary problems.

![3.16](https://cdn.mathpix.com/snip/images/UpBX8fY7X7N3siKiY_35XFxblrzXaf55ywk9uNjij1Q.original.fullsize.png)
Figure 3.16: The prediction technique consists of predicting the value of the interaction variable for the next step and use it to solve the other field. 

![3.17](https://cdn.mathpix.com/snip/images/6HIRX6k3L61rBxaTrVIhpgVM1a5FpIbaU7NlfpQwkYk.original.fullsize.png)
Figure 3.17: The advancing is calculating the solution of the next time step $\left(S_{1}\right)$ using the calculated or predicted solution of other subsystem $\left(S_{2}\right)$.

![3.18](https://cdn.mathpix.com/snip/images/U29pfayF2g0iVmQCUISOAbfXUT1AHCpsRtcESQ7209k.original.fullsize.png)
Figure 3.18: The substitution technique consists of substituting the calculated interaction variables in the field $S_{1}$ into the field $S_{2}$ to calculate it separately.

![3.19](https://cdn.mathpix.com/snip/images/p3AqIByOJGZ3V_uM_4Sa3VejUi3w53V0emMterna21M.original.fullsize.png)
Figure 3.19: The correction consist of replacing the predicted solution with recently calculated one and resolve the subsystem to obtain a better solution.

![3.20](https://cdn.mathpix.com/snip/images/Iq9a7eLhn6sfgRPlZlCssc4GNOS1QtxWHl_-96kN_nQ.original.fullsize.png)
Figure 3.20: An staggered method for solving a coupled system.

![3.21](https://cdn.mathpix.com/snip/images/1ddIo6RqUxZ_BjXkKY1OjT2x80_Xv_CThFmyDdNPT4s.original.fullsize.png)
Figure 3.21: Strategy Patterns's structure

![3.22](https://cdn.mathpix.com/snip/images/HYSG57Y-nXuDk52lPxZfJSbQW-a9OiJy6rBhWBCmvUM.original.fullsize.png)
Figure 3.22: Structure designed for linear solvers using strategy pattern

![3.23](https://cdn.mathpix.com/snip/images/Npp44DxBjMFpC4P2VIK482_oW-3WqPK14RQysl7dB1A.original.fullsize.png)
Figure 3.23: Bridge pattern's structure.

![3.24](https://cdn.mathpix.com/snip/images/YWDmcDLhpkxT5haY951lihS8fBrJDbkV_-RrWceErFM.original.fullsize.png)
Figure 3.24: Element's structure using `bridge` pattern.

![3.25](https://cdn.mathpix.com/snip/images/P7IILJ6-XQ-7K30wSyf1KXRqCTVID6k3JsCLoefSZhw.original.fullsize.png)
Figure 3.25: Composite pattern.

![3.26](https://cdn.mathpix.com/snip/images/HhkyTTr5_fiijqLN9MVba6t6bUTk5iETcCt83A3XQWk.original.fullsize.png)
Figure 3.26: Applying composite pattern to processes' structure. 

![3.27](https://cdn.mathpix.com/snip/images/e7DAEI_aZsuCqQX9j4seaBRl_pXVE_nz92vUNPvWfsQ.original.fullsize.png)
Figure 3.27: `Template Method` pattern structure.

![3.28](https://cdn.mathpix.com/snip/images/McFRGnvYT5SoteEP7Ipq2KF7k4QXWEZ2WgLmKNnXtWo.original.fullsize.png)
Figure 3.28: `Template Method` pattern applied to solving strategy.

![3.29](https://cdn.mathpix.com/snip/images/QfAsNgL8sHbgV5PesGL-pBDps6CLFtoYh381WEj-Ckw.original.fullsize.png)
Figure 3.29: `Prototype` pattern.

![3.30](https://cdn.mathpix.com/snip/images/YT_zxM_bWUw65ssJvDIUzm_BFcsJANz29gFS3PUMbN4.original.fullsize.png)
Figure 3.30: Using prototype pattern in IO for creating elements.

![3.31](https://cdn.mathpix.com/snip/images/Wzzyi14SqplIPdhs4RbCk2hxv7TrI6iTonPeN9YPCx0.original.fullsize.png)
Figure 3.31: `Interpreter` pattern structure

![3.32](https://cdn.mathpix.com/snip/images/eza6lKfqH1yWyym2aB6ZOYlZ5H13ilJ3izsV2iDxtZQ.original.fullsize.png)
Figure 3.32: Curiously recursive template pattern.

![3.33](https://cdn.mathpix.com/snip/images/Dv3BxdtFhzWihhGKxjThSYzF6MXD4g9QBnH-Rgzd48A.original.fullsize.png)
Figure 3.33: Using CRT pattern in matrix structure desing. 

![4.1](https://cdn.mathpix.com/snip/images/l86CI2-RLpx_9Gs5cOxGeLD4AM_YfsAKjOox_S5yHK4.original.fullsize.png)
Figure 4.1: Main classes defined in Kratos.

![4.2](https://cdn.mathpix.com/snip/images/SV4M9qExzYawOOjeV0m8JlQeis5I-rbhyntEFoRDq6c.original.fullsize.png)
Figure 4.2: Dividing the structure into layers reduces the dependency. 

![5.1](https://cdn.mathpix.com/snip/images/O_gnTovzOz0q0cWVWRU1t2gWKJXdjupFXuZDtoG2Z3I.original.fullsize.png)
Figure 5.1: `Quadrature` overall scheme

![5.2](https://cdn.mathpix.com/snip/images/duV0oYM5w1YTFezH0LkcfH6goOlz6Gv_xHjTgxyneIQ.original.fullsize.png)
Figure 5.2: `IntegrationPoint` class

![5.3](https://cdn.mathpix.com/snip/images/QyYj7C7OUPvWBj3EyiNFg6bPIfPF-KQEmQ6fiD2xBas.original.fullsize.png)
Figure 5.3: `IntegrationPoints` class

![5.4](https://cdn.mathpix.com/snip/images/RGN5ds1ZVDdKLP3m62hG18XBuI-IeLAftYolpdjZMyE.original.fullsize.png)
Figure 5.4: The `Quadrature` class

![5.4](https://cdn.mathpix.com/snip/images/a-QXvHTPova4RR-pO_DphWHyTyC2bkZlv4Ii-qTDR7A.original.fullsize.png)
Figure 5.5: Linear solver's structure using Strategy pattern.


![5.6](https://cdn.mathpix.com/snip/images/gObfmEZAfgtiEBIjwuepnlAGWr7H9Nm1VRx9pyy0VRI.original.fullsize.png)
Figure 5.6: Separating direct solvers from iterative solvers.


![5.7](https://cdn.mathpix.com/snip/images/GxW00vDuxzrWA9mQ1J1o7efugbmWybDZqCa-48MMXqE.original.fullsize.png)
Figure 5.7: Using the `Strategy Patterns` for designing the structure of reorderers.

![5.8](https://cdn.mathpix.com/snip/images/YI9nYwjtS6lc8XTgIPFf-ikFDv4AQkBByi3MGdQ97nQ.original.fullsize.png)
Figure 5.8: Applying the `bridge` pattern for connecting linear solvers and reorderers.

![5.9](https://cdn.mathpix.com/snip/images/u7Eu7VpMp7HSqQ26y9_GNhSWGP0q27ajbB3cf1x0KUw.original.fullsize.png)
Figure 5.9: `Strategy Patterns` is used for designing the structure of preconditioners. 

![5.10](https://cdn.mathpix.com/snip/images/tuDaGho2QhJlyEPPjGM0S7pmKggSPJo3Vx1QAXoVeOI.original.fullsize.png)
Figure 5.10: Applying the `bridge` pattern in connecting iterative solvers and preconditioners.

![5.11](https://cdn.mathpix.com/snip/images/3iAN5b8h2v7xZaRJPM35uftmlk8qtc3a_xBRwWGQw94.original.fullsize.png)
Figure 5.11: Designing the geometries structure, using the strategy pattern.

![5.12](https://cdn.mathpix.com/snip/images/n1FIsfqKO_OV9KjfSq8lvKyUuys_Gk2yYIX_ZRNvbM0.original.fullsize.png)
Figure 5.12: `Composite` pattern lets users combine different geometries in one complex geometry.


![5.13](https://cdn.mathpix.com/snip/images/GuFUMvF-vk676WCfwRwzkYjw01KDbZdOF0qM__SKsXQ.original.fullsize.png)
Figure 5.13: GeometryData class and its attributes.



![5.14](https://cdn.mathpix.com/snip/images/dzS2DIUmdAytT7WxrjbkaiHL4ODQAfmg82ORj_-b4lI.original.fullsize.png)
Figure 5.14: `Geometry` uses `GeometryData` for providing constant information and its derived classes only implement the rest of operations. 

![6.1](https://cdn.mathpix.com/snip/images/9qQaxbPX5cOmF3JcarP5MR3nLStz_Y7QePDQGxMCKw0.original.fullsize.png)
Figure 6.1: `Variable` classes structure


![6.2](https://cdn.mathpix.com/snip/images/vGBdLKyG67S2Bfd0jmr639qpgn3PpGmyYePUlAAft4g.original.fullsize.png)
Figure 6.2: VariableData class


![6.3](https://cdn.mathpix.com/snip/images/ECiz3Auh2-uaDeACYO_Ukui9Owa9s300mUXGqAEHcE0.original.fullsize.png)
Figure 6.3: Variable class


![6.4](https://cdn.mathpix.com/snip/images/Xfmi8o5j1jzlxhDRAac9ZZUL7MnRocL7YsRDp5Tk430.original.fullsize.png)
Figure 6.4: VariableComponent class


![6.5](https://cdn.mathpix.com/snip/images/SztkRDtNK7mGtMxRA8Us7PMpzH5dasewfJA1vBX-FCQ.original.fullsize.png)
Figure 6.5: Adaptor class


![7.1](https://cdn.mathpix.com/snip/images/Qb8YNyjKbXGhONWGLGWgBF7pk9Q3sHjZ4YJbKBnt_4M.original.fullsize.png)
Figure 7.1: An heterogeneous container in memory storing a `double`, a matrix, an integer, a vector and also an string.


![7.2](https://cdn.mathpix.com/snip/images/sTzp3lPMfewZD2fe4R3ZsZ3T9Bw1jXAjgvQfJ8W8gbQ.original.fullsize.png)
Figure 7.2: Array elements sequentially stored in memory

![7.3](https://cdn.mathpix.com/snip/images/oqnus-aKzdvkqcQOF6WIUBfCXjQuhVs51GMHsJUrO2g.original.fullsize.png)
Figure 7.3: Array indexing

![7.4](https://cdn.mathpix.com/snip/images/3Qz48ktVIoLi018hdJ9QdhTKqUFeqoZVJDulmqtqedc.original.fullsize.png)
Figure 7.4: Array constructed with given capacity

![7.5](https://cdn.mathpix.com/snip/images/TKbrdyBRxTVLjPJ4I46bk9kXSQNlPfLO68wfBbaINqU.original.fullsize.png)
Figure 7.5: Array indexing

![7.6](https://cdn.mathpix.com/snip/images/b2tX39SvDuCaUQJG8O5QBm_YTlyRnTvH5bB_6AKPwbw.original.fullsize.png)
Figure 7.6: Adding an element in second position which causes all the rest of elements to move one step forward to make room for new one

![7.7](https://cdn.mathpix.com/snip/images/q0aQrX4M23p3b4SrcFVY2Dryc9RmocJjAIvaL7jf8fM.original.fullsize.png)
Figure 7.7: Appending an element to the end of array

![7.8](https://cdn.mathpix.com/snip/images/mrzOOnz5nSifG2LwrWNx932s1kVcE9ognnMe-3CiTU4.original.fullsize.png)
Figure 7.8: Erasing the second element of array

![7.9](https://cdn.mathpix.com/snip/images/ZD854yMPmhmKsPBXPXA0QWl68-GCoCApXPNqYDqTmfg.original.fullsize.png)
Figure 7.9: Resizing an array beyond its capacity when there is no space after array to grow

![7.10](https://cdn.mathpix.com/snip/images/TAJkJkmCl2mz42vSmWXYyTC95_XK1HgmuePISO_7-Qk.original.fullsize.png)
Figure 7.10: Array indexing

![7.11](https://cdn.mathpix.com/snip/images/jtjhIm9tuOkSS8Ks6qr81nA2vx79jYB2SXxWwNh-DFQ.original.fullsize.png)
Figure 7.11: Adding an element in second position which causes all the rest of elements to move one step forward to make room for new one

![7.12](https://cdn.mathpix.com/snip/images/0ZbZqBe1-4CSNUGLLfogfAo0M3GlDHAZjg-lfCz4d-I.original.fullsize.png)
Figure 7.12: Appending an element to the end of array

![7.13](https://cdn.mathpix.com/snip/images/QUfNl38TKy8aqFO6VIrCSTY3RdIV_ACyU-Yqkthzc0A.original.fullsize.png)
Figure 7.13: Erasing the second element of array

![7.14](https://cdn.mathpix.com/snip/images/6AAJP4dRIFwQHuxxNJsegibfC1Le5JdHFNPLkTps6c4.original.fullsize.png)
Figure 7.14: Resizing an array beyond its capacity when there is no space after array to grow

![7.15](https://cdn.mathpix.com/snip/images/ripNRyGH5q6oiyYDT0QJ4Me1ap6PrGPCHywtrjQU9gA.original.fullsize.png)
Figure 7.15: Singly linked list stored individually each element with a link to the next element

![7.16](https://cdn.mathpix.com/snip/images/x0pyQRPTL9zUwtGn2bTTKzi1KpIU-vuV_c50RGFd2WM.original.fullsize.png)
Figure 7.16: Inserting a new element after first element of the list

![7.17](https://cdn.mathpix.com/snip/images/ASUA6KdFQ_67P4b9TzFuGYBkFyIMLohfmv8LQIDWxjk.original.fullsize.png)
Figure 7.17: Appending a new element after last element of the list

![7.18](https://cdn.mathpix.com/snip/images/ZjC_LelowFzs6Wklg6ptYVmGpsBOo_65V-9fr5VuqQU.original.fullsize.png)
Figure 7.18: Erasing an element from list

![7.19](https://cdn.mathpix.com/snip/images/7p8wqQu_FLQ-XQtVe8Ha9jnbsDJuB-gw59-JQYtNeqA.original.fullsize.png)
Figure 7.19: Swapping two list by changing their head pointer

![7.20](https://cdn.mathpix.com/snip/images/1maE-VMGmE-wq6ag7bAERsj35Mk_yipGYYIjHxXNZRI.original.fullsize.png)
Figure 7.20: A doubly linked list in memory

![7.21](https://cdn.mathpix.com/snip/images/MH5Z_RVONBx7XEtCVkXz-kNnM0U9mhFFVWJBm1WNj7k.original.fullsize.png)
Figure 7.21: Inserting a new element after first element of the list

![7.22](https://cdn.mathpix.com/snip/images/WxnPdncoL5bje-dq5rFiMVA3UQxK7mUgm8xSOeimyaA.original.fullsize.png)
Figure 7.22: Appending a new element after last element of the list

![7.23](https://cdn.mathpix.com/snip/images/rs0jlr4oIkYfaLWKP398_NnqsY9RC8lRcWlwWMeZ2Gc.original.fullsize.png)
Figure 7.23: Erasing an element from the list

![7.24](https://cdn.mathpix.com/snip/images/dpSiCdTVlBH7QWA5YoZx4v1HCOEKRXCYKW-19CX8zFQ.original.fullsize.png)
Figure 7.24: Swapping two list by changing their head pointer

![7.25](https://cdn.mathpix.com/snip/images/EDpbHXcaOJ3rXjXzN7rkXj8GOXKla4DgcHZWEoqBI0Y.original.fullsize.png)
Figure 7.25: A binary tree

![7.26](https://cdn.mathpix.com/snip/images/DAE8awruheNIb-rnB6eFe51OHKiNtaMZrIFBQcN3V6o.original.fullsize.png)
Figure 7.26: Two different binary trees because the position of node $\mathrm{b}$ is changed from being left child to be right child

![7.27](https://cdn.mathpix.com/snip/images/bxOPspR2g1WXa3UxF7U0Kg1k0DjhplQ8DhqmGmX9Z44.original.fullsize.png)
Figure 7.27: A binary tree containing a set of numbers ordered respect to the less than operator $<$

![7.28](https://cdn.mathpix.com/snip/images/shMmgUEVKjq2ai3Uc8Fri7SoCLfGdIHL2LndTS8nAf4.original.fullsize.png)
Figure 7.28: Inserting a new value in the tree.

![7.29](https://cdn.mathpix.com/snip/images/wg2lcjK9Vc1HPdpLy_E-b_GjbS4hpG-W4Fbikld0iZw.original.fullsize.png)
Figure 7.29: Finding a value in a binary tree

![7.30](https://cdn.mathpix.com/snip/images/c34pDBwCti2r9OqSxOrzZAdWs30ucC9r6dv9iv5kFaA.original.fullsize.png)
Figure 7.30: The binary tree after erasing an element

![7.31](https://cdn.mathpix.com/snip/images/L8UBca_kjRiifZSVjwXRiFwe5y8z2eOUxcYG3wuFasU.original.fullsize.png)
Figure 7.31: Iterator path from elements 1 to 12

![7.32](https://cdn.mathpix.com/snip/images/k4LcvwsaR5yT9VJWDhNjvAHvG3KJwKTKJHdwDe42uEA.original.fullsize.png)
Figure 7.32: In a general quadtree each node has up to four children

![7.33](https://cdn.mathpix.com/snip/images/FvOq_M3Pi_hix87egRIRuECtgz8xhlsYKRfFIqk-4ug.original.fullsize.png)
Figure 7.33: Quadtree can be used to partition a domain into layers of sub-domains.

![7.34](https://cdn.mathpix.com/snip/images/6m-ix9Gokqr5exI1ZTxQx6iolQlVGoHWCjoZdBAB_AI.original.fullsize.png)
Figure 7.34: Ordering two dimensional points in quadtree. 

![7.35](https://cdn.mathpix.com/snip/images/q1BYTfojyYjUK-MOWlBUKdRp9qS6xjMWUdNMh9G9P4Q.original.fullsize.png)
Figure 7.35: Partitioning a two dimensional domain using k-d tree.

![7.36](https://cdn.mathpix.com/snip/images/yMeft8RYj6gPLcvx1gP4QOMk7p_-BayDdAcb8YzGnXY.original.fullsize.png)
Figure 7.36: Partitioning a two dimensional domain using bins.

![7.37]](https://cdn.mathpix.com/snip/images/T6PxAGZnzg4cHte3LEuWNstUkomDJm1ghahpfcRlIhA.original.fullsize.png)
Figure 7.37: Bins structure.

![7.38](https://cdn.mathpix.com/snip/images/30ipLrPXCvXPY-cPqc3zEqRrGDjZS46gUw98m2Kl6oo.original.fullsize.png)
Figure 7.38: Memory use comparison a) Comparing memory use of vector<`double`>, list<`double`> and set<`double`> b) Comparing memory use of vector<pair<int,`double`> >, list<pair<int, `double`> > and map<int, `double`>

![7.39](https://cdn.mathpix.com/snip/images/ZZVJTLJkF-5FF1BJ9H33uo_xBiZcM4n1opg0rKWQJC8.original.fullsize.png)
Figure 7.39: Memory use comparison between arrays with size $n=100000$ of different containers. a) Comparing memory use of vector<`double`>, list<`double`> and set<`double`> b) Comparing memory use of vector<pair<int, `double`> >, list<pair<int, `double`> > and map<int, `double`> 

![7.40](https://cdn.mathpix.com/snip/images/XBgAlza4OhzyU4X_rQW8ntpg276EIVMNnvh0b6mFHe4.original.fullsize.png)
Figure 7.40: Construction time for different containers. a) Comparing construction time for vector<`double`>, list<`double`> and set<`double`> b) Comparing construction time for vector<pair<int, `double`> >, list<pair<int, `double`> > and map<int, `double`>

![7.41](https://cdn.mathpix.com/snip/images/LIqLjCL3WbTnzmMqrMdVhRqQhZKE4U8u-FXeSuGDcBc.original.fullsize.png)
Figure 7.41: Destruction time for different containers. a) Comparing construction time for vector<`double`>, list<`double`> and set<`double`> b) Comparing construction time for vector<pair<int, `double`> $>$, list<pair<int, `double`> $>$ and map<int, `double`> that a container has to 
be created and deleted immediately.

![7.42](https://cdn.mathpix.com/snip/images/dT047so4Hp7zVFe8Ne3dLM2cHRr_zNEx9Ab5dpQb4RY.original.fullsize.png)
Figure 7.42: Construction and destruction time of containers defined as local variables and allocated in stack. a) Comparing construction/destruction time for `double`*, vector<`double`>, list<`double`> and set<`double`> b) Comparing construction/destruction time for vector<pair<int, `double`> >, list<pair<int, `double`> > and map<int, `double`>

![7.43](https://cdn.mathpix.com/snip/images/v7B5uu0Di_dVsBKiTdqUP031ngJhMF_iPmlfdT4G-Pk.original.fullsize.png)
Figure 7.43: Iterating time for $10^{9}$ steps of iterations with different containers. a) Comparing performance of `double`*, vector<`double`>, list<`double`> and set<`double`> b) Comparing performance of vector<pair<int, `double`> >, list<pair<int, `double`> > and map<int, `double`>

![7.44](https://cdn.mathpix.com/snip/images/jhRLEjpTMtkXTkO8qTVN-5WnxpoD8_XqG2AQR-c-Y9w.original.fullsize.png)
Figure 7.44: Time comparison for $10^{4}$ elements pushback to different containers. a) Comparing performance of vector<`double`>, list<`double`> and set<`double`> b) Comparing performance of vector<pair<int,`double`> $>$, list<pair<int, `double`> > and map<int, `double`>

![7.45](https://cdn.mathpix.com/snip/images/TvjmUi-EtE2d5GOhizSdZgFPOTUSjCSEKYEjuvQrL0M.original.fullsize.png)
Figure 7.45: Time comparison for $10^{4}$ pushfronts to vectors with different sizes. a) vector<`double`> b) vector<pair<int, `double`> > 

![7.46](https://cdn.mathpix.com/snip/images/4xPzplNZ6zDxfSVw4_IZfh6YdaI8mMS7qZhqZ3lGqnY.original.fullsize.png)
Figure 7.46: Time comparison for 100 calls to copy constructor. a) Comparing performance of vector<`double`>, list<`double`> and set<`double`> b) Comparing performance of vector<pair<int, `double`> >, list<pair<int, `double`>> and map<int, `double`>

![7.47](https://cdn.mathpix.com/snip/images/wNTmQ3BS2Z9RJruIap9NqflBZdzRszAZ4T9XfWKGnK8.original.fullsize.png)
Figure 7.47: Time comparison for different searching algorithms over sorted and unsorted containers a) Comparing performance of vector<`double`> with brute-force, list<`double`> with bruteforce, set<`double`> binary tree search and sorted vector<`double`> with binary search. b) Comparing performance of vector<pair<int, `double`> > with brute-force key finding, list<pair<int, `double`> > with brute-force key finding and map<int , `double`> binary tree search. 

![7.48](https://cdn.mathpix.com/snip/images/_rekt-eo16hYb-JJRv706mzDb9EfZN9IZtElFuVlkUk.original.fullsize.png)
Figure 7.48: Time comparison for different searching algorithms over sorted and unsorted small containers a) Comparing performance of vector<`double`> with brute-force, list<`double`> with bruteforce, set<`double`> binary tree search and sorted vector<`double`> with binary search. b) Comparing performance of vector<pair<int, `double`> > with brute-force key finding, list<pair<int, `double`> > with brute-force key finding and map<int, `double`> binary tree search.

![7.49](https://cdn.mathpix.com/snip/images/X2qPhMs-b3ygdc1J8vs7tUmrC9XXK4AsKwZwdlnHlyI.original.fullsize.png)
Figure 7.49: Combining containers for holding doubles, vectors, matrices and complex numbers.

![7.50](https://cdn.mathpix.com/snip/images/8m2HQhFToZA3aQ8e8qE7Z__gkIYfN8DuZao_y6TeYQ4.original.fullsize.png)
Figure 7.50: Implementing separate access interface for each type in a compound container

![7.51](https://cdn.mathpix.com/snip/images/oeET2B6hjjx1iCn3O7vQ1-SRgRlg6ac7TXuKCB1_RTc.original.fullsize.png)
Figure 7.51: Container with three sub-containers as its attributes.

![7.52](https://cdn.mathpix.com/snip/images/zjavh1L9WTvemHgjScUe0xMb_4KTopzdgcURo9CufrQ.original.fullsize.png)
Figure 7.52: Combining different containers using multiple hierarchy.

![7.53](https://cdn.mathpix.com/snip/images/PzLVm7SFYTAd3zNWTAOJvnlbNsa1MrGI5vajBRL0okg.original.fullsize.png)
Figure 7.53: Heterogenous container uses different handlers to access data.


![7.54](https://cdn.mathpix.com/snip/images/wWxe7dkarz8BnN7ERmmEiB7z9gXIgwNfiW15u-nTddU.original.fullsize.png)
Figure 7.54: Data value container uses the `Variable` class to process its data.

![7.55](https://cdn.mathpix.com/snip/images/UFuFO7xiMC53xrYixY5EuetQe2hSU2CXUTYWs3SrAYw.original.fullsize.png)
Figure 7.55: A data value container with continuous memory.

![7.56](https://cdn.mathpix.com/snip/images/CsNQGtVIUqar4QIcuIxOXLIRcpSolrHYa4-vzTlRpHQ.original.fullsize.png)
Figure 7.56: A data value container with discontinuous memory.

![7.57](https://cdn.mathpix.com/snip/images/djk1Kw-eGO8QcOLLx5PB2MOubRz8TN1H_LswNp8Wh00.original.fullsize.png)
Figure 7.57: Shallow copying a pointer results in shared data for source and copied vectors.

![7.58](https://cdn.mathpix.com/snip/images/tDiGhgkYmnbEtXce41o07_7TwEXT5Sh2xWEA8AyK5HE.original.fullsize.png)
Figure 7.58: Deep copying results in an individual copy of source vector.

![7.59](https://cdn.mathpix.com/snip/images/hxleGlRPS1N8Efr7-tJriwZIMaBLU-L5ouYEV_Ue88A.original.fullsize.png)
Figure 7.59: Accessing to a value in the variables list container.

![7.60](https://cdn.mathpix.com/snip/images/DD8k7P4DTBQV22tUC8lN5K6_3HL7wSTutGnmyKCawl8.original.fullsize.png)
Figure 7.60: The VariablesList class provides the list of variables and their local positions for the VariablesListDataValueContainer.

![7.61](https://cdn.mathpix.com/snip/images/6JcYF3m7gVIMixcHs3GoiW5lU4s7LTQkLVZjYLeFlmU.original.fullsize.png)
Figure 7.61: Grouping all variables of one `Node` or `Element` in order to reduce the cache miss in nodal and elemental operations.

![7.62](https://cdn.mathpix.com/snip/images/tIra-kT8_1BSrpHae_dgtzDKWwAQnj1QWxAxjp7KKh0.original.fullsize.png)
Figure 7.62: Storing values of each variable in different `Nodes` or `Elements` sequentially to optimize the data structure for algorithms working with one variable over the domain.

![7.63](https://cdn.mathpix.com/snip/images/hpV5Y6TRBrOqFHfda_1Hn4P8nDA8V5QhlQo9bw29_ZE.original.fullsize.png)
Figure 7.63: Separating the components of each variable for algorithms working with each component separately.

![7.64](https://cdn.mathpix.com/snip/images/cQNIwALoiET0HAfXct7UVW1x0YE6auIi7U-l0Br4v6U.original.fullsize.png)
Figure 7.64: In an entity base data structure all data related to one entity can be stored with entity as its members.

![7.65](https://cdn.mathpix.com/snip/images/5pjAs37kSlLzC3zT29F8eguUQB1qy5D1YjRpVj-POkQ.original.fullsize.png)
Figure 7.65: Each entity has a reference to its container or block of data.

![7.66](https://cdn.mathpix.com/snip/images/9hhIb20Jc3V9YOHRFyPI-x-YTP11R7yVmzR-e_SXLRo.original.fullsize.png)
Figure 7.66: Nodal, elemental and conditional data containers with properties are the basic units of Kratos data structure.

![7.67](https://cdn.mathpix.com/snip/images/fhStNtD0VQEoTbtxvHAy7VT2EmWOVpP8z0OpPm4k9ZE.original.fullsize.png)
Figure 7.67: Separate containers for `Nodes`, `Properties`, `Elements` and `Conditions` can be used to group each type of entities and then process themselves or their accessible data.

![7.68](https://cdn.mathpix.com/snip/images/D8uWmGb-jBbl0nAebxhWhiqfM4rJ2Fl-J8kYr45tjZ8.original.fullsize.png)
Figure 7.68: `Mesh` is a complete pack of all types of entities without any additional data associated with them.

![7.69](https://cdn.mathpix.com/snip/images/St-p3bU9k552AY0a4jRzjwkAZEH85JSoejk5n5kE_UM.original.fullsize.png)
Figure 7.69: ModelPartholds `Mesh` with some additional data referred as `ProcessInfo`

![7.70](https://cdn.mathpix.com/snip/images/EsywhpjYv9GE2Q4KzODENjaTF-QcDf2vuFWH5Di3w5M.original.fullsize.png)
Figure 7.70: Using buffer for all variables results memory overhead due to redundant copies of no historical variables.

![7.71](https://cdn.mathpix.com/snip/images/L186xsOOPmlW6cgD4NwTJtAzuvTrRAnEsbEGJyvLlbg.original.fullsize.png)
Figure 7.71: The first improvement is dividing nodal data structure into two different containers, nodal data (no historical data) and solution step nodal data (historical data).

![7.72](https://cdn.mathpix.com/snip/images/9DEc7uwlYFE68JMzl1Qm5vIFAMOii1O8DWv5OXO37Mw.original.fullsize.png)
Figure 7.72: The current structure allocates all buffer data in a block of memory to reduce the cache misses produced by memory jumps and also to provide a compatible data with other libraries.

![7.73](https://cdn.mathpix.com/snip/images/j3NXt2dQCXKpLK3X7Nmgbb2WmRHB7mx_3eycNtsloBE.original.fullsize.png)
Figure 7.73: Different `Elements` or `Conditions` use `Properties` as their share data container. This avoids redundant copies of data in memory.

![7.74](https://cdn.mathpix.com/snip/images/CRJmBngpbYhFwp78VbrF1yrNynelJCaXlnYoA4WZUjA.original.fullsize.png)
Figure 7.74: Different Meshes can share their entities' containers.

![7.75](https://cdn.mathpix.com/snip/images/Yx3YZMvSHCRXgmA26y5z51epKVdovGfQByVi0_VSpzo.original.fullsize.png)
Figure 7.75: `ModelPart`'s structure. 

![7.76](https://cdn.mathpix.com/snip/images/mc0ehT6-RCci2cglH5HOCv6vsDf-fjg2hQ41BWP8bsg.original.fullsize.png)
Figure 7.76: `ProcessInfo`'s linked list mechanism for holding history of solution.

![7.77](https://cdn.mathpix.com/snip/images/zrOcxxYaR32TxflxPMClMyP1cDVbxqXkKUcMhEbRykw.original.fullsize.png)
Figure 7.77: `ModelPart` can share its Meshes with other model parts.

![7.78](https://cdn.mathpix.com/snip/images/ksIesBI_fKiFmMTqJaAaGkCEt9ZYEooYP4ks9EIrUGM.original.fullsize.png)
Figure 7.78: `ModelPart` manages the variables list for its entities.

![8.1](https://cdn.mathpix.com/snip/images/usXB97wg5g0MYVdt6mtfyCLMp6sElI89IqRct2K3Gzw.original.fullsize.png)
Figure 8.1: `Elements`' structure using strategy pattern.

![8.2](https://cdn.mathpix.com/snip/images/ZrZbNXvpzFywkh3hENEfp0GxClFS-2WxJi53fyRJHXs.original.fullsize.png)
Figure 8.2: Deriving `Element` from geometry requires several `Elements` with same formulation but different geometries to be created.

![8.3](https://cdn.mathpix.com/snip/images/vq_fAcIT7xC2OAHbFKIXurSNIfmVGRODUMQeOUHKGAQ.original.fullsize.png)
Figure 8.3: `Element`'s structure using the `bridge` pattern.

![8.4](https://cdn.mathpix.com/snip/images/65s_9w9GL0BO-C-YLJAITbfdc8ZF5D-_Qfi3p-vD4I8.original.fullsize.png)
Figure 8.4: `Condition`'s structure using strategy pattern.

![8.5](https://cdn.mathpix.com/snip/images/STN45Nx1Ky9N3bddZoiRbR_LK0MyBRYNdacbBbvRow8.original.fullsize.png)
Figure 8.5: `Condition`'s structure using `bridge` pattern.

![8.6](https://cdn.mathpix.com/snip/images/CFIbvFWaJ5ZKJIEEEcCl6MXxEe35B09tJLP876Sw3i0.original.fullsize.png)
Figure 8.6: `Process` structure using strategy pattern.

![8.7](https://cdn.mathpix.com/snip/images/IuFj6-2j56goexLOmev729mD8VdIHv_18npLe22RwlQ.original.fullsize.png)
Figure 8.7: Applying composite pattern to the `Process` structure.

![8.8](https://cdn.mathpix.com/snip/images/IPcWtWIToIXZJqkU0PVwezDQ7OBlVRVC36b_gVrfkQg.original.fullsize.png)
Figure 8.8: The reduced composite structure for `Process`.

![8.9](https://cdn.mathpix.com/snip/images/gD6TjrvZ1FtM5SJI35hXorLRAD53TSfGHOgl7C__Srw.original.fullsize.png)
Figure 8.9: SolvingStrategy uses the structure designed for `Process`.

![8.10](https://cdn.mathpix.com/snip/images/yVxCaXFihG4J4EDOdhEmc7PKJh2qHU38sOzhOjh06JI.original.fullsize.png)
Figure 8.10: `Template Method` pattern applied to solving strategy.

![8.11](https://cdn.mathpix.com/snip/images/OoBW9iZdXL2j7BkwCWNEnDLcCZOE-fV0MwACS7NfAvE.original.fullsize.png)
Figure 8.11: Deferring different parts of the algorithm to BuilderAndSolver and Scheme.

![9.1](https://cdn.mathpix.com/snip/images/oU-Lkeu4m1MigRgh_Py7c-RLpIvv5aMO1-2lMfHqTRw.original.fullsize.png)
Figure 9.1: A Console IO interface

![9.2](https://cdn.mathpix.com/snip/images/nUfJfohGnnHyRE-abVZ_6r15JeUcYH4abPwx2Bi7onU.original.fullsize.png)
Figure 9.2: A file IO interface



![9.3](https://cdn.mathpix.com/snip/images/XRcabOF5rZ0lmVkIHbzUIQyhkBTQ9tDg7Z6muKrBz0s.original.fullsize.png)
Figure 9.3: A multi-media interface

![9.4](https://cdn.mathpix.com/snip/images/yndIzdeZWGWbVxRNhcgEnkcFe9ZObN2y4ASStxGZObQ.original.fullsize.png)
Figure 9.4: An Extendible IO interface

![9.5](https://cdn.mathpix.com/snip/images/McbPnoBIOP15eplXp0DDUc3ai3l4uppmNciteT4pBQ0.original.fullsize.png)
Figure 9.5: Extended IO

![9.6](https://cdn.mathpix.com/snip/images/_QLYFq76Qa91j1qz9Oip1uZwjTWxxM_hwckhRBoUSdo.original.fullsize.png)
Figure 9.6: A single IO procedure

![9.7](https://cdn.mathpix.com/snip/images/c5QSNBHadExARAn1UXeV0aFoFBAHPwzAs1_MSfLX5AA.original.fullsize.png)
Figure 9.7: Repeating IO procedure

![9.8](https://cdn.mathpix.com/snip/images/GFf_U1Ej_KEQ0wRI_X4-erxstLVG621bh0Bh4brTTBE.original.fullsize.png)
Figure 9.8: Complex IO procedure

![9.9](https://cdn.mathpix.com/snip/images/ycqE41YgNHiSdLMp3ETFVunq-cf028upPFfcTafwpic.original.fullsize.png)
Figure 9.9: Using handlers to extend type supporting

![9.10](https://cdn.mathpix.com/snip/images/UdkQdj_Kf4wM04wHS39y1Vx7A9D9-kLU29i1gLm-xaM.original.fullsize.png)
Figure 9.10: Writing a number to text format

![9.11](https://cdn.mathpix.com/snip/images/EvrDQX45dk4VoO9XXRxBIg19jVwB7_7oc9zVAsxzLnM.original.fullsize.png)
Figure 9.11: Reading a number from text format

![9.12](https://cdn.mathpix.com/snip/images/Pxmv2T6mR1-wQNjv-Ge8aTeAdb-IjrejbqUiL_4DG3c.original.fullsize.png)
Figure 9.12: Writing a number as binary format

![9.13](https://cdn.mathpix.com/snip/images/WG_KLDYPwYVWkN_vcv1RiIJRRYQUPUEki74ZBtOBTK0.original.fullsize.png)
Figure 9.13: Reading a number from Binary format


![9.14](https://cdn.mathpix.com/snip/images/hzKj_1vW0iEwJce3js279-mgg6cz-fO2AA-S2Kv2myM.original.fullsize.png)
Figure 9.14: IO single format example

![9.15](https://cdn.mathpix.com/snip/images/gz1UNtP-P0edILOjAtRZiP-6J_0PP7XM0JZD8zZdesg.original.fullsize.png)
Figure 9.15: IO dual format example with specialized methods

![9.16](https://cdn.mathpix.com/snip/images/TFudSRKmY4XmMKhvRnBmtofFefhgbH12ingffYz-0oA.original.fullsize.png)
Figure 9.16: IO dual format example with passing type as argument



![9.17](https://cdn.mathpix.com/snip/images/b-W3tyTDkPLaui5M3-xMCWn5TqoiQGqG_Fdp23cVItM.original.fullsize.png)
Figure 9.17: Multi format extendible IO example



![9.18](https://cdn.mathpix.com/snip/images/gQ62cOo-mImla9og2LyH76VQysMhqZhYn3ckT-aM36A.original.fullsize.png)
Figure 9.18: Extended IO to a new format

![9.19](https://cdn.mathpix.com/snip/images/6DCSOCV7s1OLLgA8Vat2-r_lsLBuVDaaDvg0abRPu6U.original.fullsize.png)
Figure 9.19: A simple `Mesh` class and its components


![9.20](https://cdn.mathpix.com/snip/images/NNduBimvTXvCAtlmdmTaZ6MjpCM6BLeyl9UwIcGKAoU.original.fullsize.png)
Figure 9.20: Adding the Serialize method to create necessary interface 

![9.21](https://cdn.mathpix.com/snip/images/wewP0l6SszjX20hGb45Y57w6MOkbhMeyrtbzMQybqmM.original.fullsize.png)
Figure 9.21: Pseudo implementation of Serialize methods.


![9.22](https://cdn.mathpix.com/snip/images/CTE-xYxyAyVL0XQmhW2w039onOKU2-W6zbELYlfg5z8.original.fullsize.png)
Figure 9.22: Data sequence in output file resulting from the `Mesh` serialization example

![9.23](https://cdn.mathpix.com/snip/images/tIHC8vyIGaRKAhT1yD2tJ6QQJHT95470IevhoNc1dXs.original.fullsize.png)
Figure 9.23: Multi format IO structure

![9.24](https://cdn.mathpix.com/snip/images/pVb1nY0eqQMwvjPESoGPpZVOD0zaFq2uyiJI38JRIzk.original.fullsize.png)
Figure 9.24: Multi format and multi media IO structure


![9.25](https://cdn.mathpix.com/snip/images/B9FU_Y8JTbgT14n6PEHTLPKVETpKOJf2aG3dfZhfAYA.original.fullsize.png)
Figure 9.25: Multi format and Medium IO structure 

![9.26](https://cdn.mathpix.com/snip/images/AG3YCmFQ_8nwMJgN4qVBUCYOptpKq4ti6LAvxTactHM.original.fullsize.png)
Figure 9.26: Extended Multi format and Medium IO structure

![9.27](https://cdn.mathpix.com/snip/images/kFQ7iiDOMlA_I2S4_Bn8UMx5_ndCpxAKTgzHBHK_MlE.original.fullsize.png)
Figure 9.27: Using prototype pattern in IO for creating `Elements`.

![9.28](https://cdn.mathpix.com/snip/images/tsk9CHAL54doPNpsJq7_UG3i-w13gte_Jz5yWGztQOw.original.fullsize.png)
Figure 9.28: Using KratosComponents in IO

![9.29](https://cdn.mathpix.com/snip/images/CK_nK5BNWyyADcdYKY2_4INVimC7B2VIgvYNm9za1zg.original.fullsize.png)
Figure 9.29: Using an external serialization library

![9.30](https://cdn.mathpix.com/snip/images/J7O8DwDNZn9C9ZZb5BdJlyA5Q-roVzCQENZ0ShNJtRc.original.fullsize.png)
Figure 9.30: Global structure of an interpreter

![9.31](https://cdn.mathpix.com/snip/images/NLaCt16pMGtQL8JgVRPn3-RL_gpT1VjcU8JpwICa55s.original.fullsize.png)
Figure 9.31: Parse tree for if statement example


![9.32](https://cdn.mathpix.com/snip/images/kjM4RtZMXe-Bt3HkBWfcCfl-gHhEMAZ2Uqq66kt8H6s.original.fullsize.png)
Figure 9.32: Finite automata example

![9.33](https://cdn.mathpix.com/snip/images/qgumB1DK_uU7QMpifuLEOMfUUTSHlj6Waw0TcJth0gg.original.fullsize.png)
Figure 9.33: Combined finite automata example

![10.1](https://cdn.mathpix.com/snip/images/OpxrnVwNeDXvBks7Y76jhvu4K9bCtH0--vD_61gQJac.original.fullsize.png)
Figure 10.1: a) `Geometry` of cylinder example, domain dimension $19.0 \times 8.0 \times 0.2$ and $R_{c}=0.5$. b) the mesh used.

![10.2](https://cdn.mathpix.com/snip/images/x99zIhZz8QrzjYklcb-KkEBZJiTtSxngMoSPSwgCHR0.original.fullsize.png)
Figure 10.2: Detail of the mesh used for the cylinder example. $\mathbf{2 6 0}$

![10.3](https://cdn.mathpix.com/snip/images/CsXQ_Zlgh6ki9QtyhT1vrjQ903jpPRVI1qWvqtWbJL0.original.fullsize.png)
Figure 10.3: Pressure at different time steps. 261

![10.4](https://cdn.mathpix.com/snip/images/7szORDtcImSATkwx8Cn-HEV4d5weKE02OojUzPqjTFg.original.fullsize.png)
Figure 10.4: Velocity at different time steps. 

![10.5](https://cdn.mathpix.com/snip/images/IC_pJM3rnRli4a__Yj2OJKadkMcXTk68jgFyJ1NtVOg.original.fullsize.png)
Figure 10.5: The comparison of results obtained by Kratos (red line) using a fractional step algorithm and FEFLO (blue line). a) 
Lift calculated for the cylinder b) Drag calculated for the cylinder.

![10.6](https://cdn.mathpix.com/snip/images/DcMktGdtYmQOtYbbPXsPClNS1G0nagIlxLKfjSM5Aoo.original.fullsize.png)
Figure 10.6: The comparison of results obtained by Kratos (red line) using a predictor corrector
scheme and FEFLO (blue line). a) Lift calculated for the cylinder b) Drag calculated for the
cylinder.

![10.7](https://cdn.mathpix.com/snip/images/8or8N-VKRaxsaLpTxxWzs_mEwh07zPN3ErtlrlZPr4E.original.fullsize.png)
Figure 10.7: Flag flatter simulation using fluid structure interaction with mesh movement.

![10.8](https://cdn.mathpix.com/snip/images/Zea1QhTiPPcIECWL8-uNv8FWvlSQKIdMYn6fHgnoHrk.original.fullsize.png)
Figure 10.8: A Dam break test and its simulation by the PFEM implementation in Kratos. [^57] 10.4. THERMAL INVERSE PROBLEM

![10.9](https://cdn.mathpix.com/snip/images/ymwUafacv9_0lbKWM77LHG6dXR_Kq6Q4MaoO1wsHQks.original.fullsize.png)
Figure 10.9: Dam break, continued. [^57] 

![10.10](https://cdn.mathpix.com/snip/images/4Dx2XolfMc9L0TNJngV36J4UzLiuaMTyLbS3Ve_Gx1c.original.fullsize.png)
Figure 10.10: General solution of variational problems using neural networks consists of three main steps.

![10.11](https://cdn.mathpix.com/snip/images/IYjA3IgtiUfsrGQwc9i_ahnH67xl_I6wG_evCU5VmBc.original.fullsize.png)
Figure 10.11: Actual boundary temperature (red), estimated boundary temperature (green) and measured temperature at the center of the square (blue) for the boundary temperature estimation problem.


![10.12](https://cdn.mathpix.com/snip/images/t4Rhi4VJPx7kkcnv9lZC4cKcB0wRFoiaQHEVoN_pDnE.original.fullsize.png)
Figure 10.12: Actual diffusion coefficient a) and estimated diffusion coefficient b) for the diffusion coefficient estimation problem.


[3.1]: https://cdn.mathpix.com/snip/images/Vuy948tmSzAF01iae-29Rg1xWXXd-u80E5x6FEXRKE0.original.fullsize.png
[3.2]: https://cdn.mathpix.com/snip/images/HqiUxi8uAuE6QefJY209KB2og9NhwoAQNj3pkDOtBRQ.original.fullsize.png
[3.3]: https://cdn.mathpix.com/snip/images/TZbrdDnDNvOaqHIbRSXRzh2jVdg2bxI5_pck2dQoehQ.original.fullsize.png
[3.4]: https://cdn.mathpix.com/snip/images/l9O4MOXzJ0KJOGbCwbGuFiPolTfKc5zSGUKENfVLcVI.original.fullsize.png
[3.5]: https://cdn.mathpix.com/snip/images/fuQctc_TafzE3Vk8e5l8U4QxxaYekrEMRgEsUTxmy00.original.fullsize.png
[3.6]: https://cdn.mathpix.com/snip/images/zuH2P2NcQ1UPmqzoNMYUMKUXW8SSsvJCBdF9vDyzmXg.original.fullsize.png
[3.7]: https://cdn.mathpix.com/snip/images/3ICGd65Cv4xWJiNTd6BmSh5_6AQAz9kY4Pr1jHsi_oI.original.fullsize.png
[3.8]: https://cdn.mathpix.com/snip/images/9wnWXqHW6J59k4oXpes8-56nRIoFAscieb6PvTPqZSs.original.fullsize.png
[3.9]: https://cdn.mathpix.com/snip/images/Aesg3nz2LqX3t-kM8mKi9zpEMSqiFBJtgaXLh_QJcTU.original.fullsize.png
[3.10]: https://cdn.mathpix.com/snip/images/q1BQ_Zi-t46b6sbGJ8_1InWZMuP5rHDoFNC_6YkbPLo.original.fullsize.png
[3.11]: https://cdn.mathpix.com/snip/images/SI2dCvzcMxUA0UZM4Myk6tmnYoAuL6bPchcU0avWZ5o.original.fullsize.png
[3.12]: https://cdn.mathpix.com/snip/images/ByeyQR2a_7vGfkMkkJKaWDQV6iaWhVTRyfWJvVtBMbA.original.fullsize.png
[3.13]: https://cdn.mathpix.com/snip/images/SfGO6nDdWpBtVXEEbpdIJwUkyiO70WOsXt-KOSukN9o.original.fullsize.png
[3.14]: https://cdn.mathpix.com/snip/images/CBw2ZLvwECkGT1mb3gmc7PY0GsMui_12LidmWikuaVI.original.fullsize.png
[3.15]: https://cdn.mathpix.com/snip/images/Po5gr8vigHHK6yc-bgU0UWbGFGpSEXinDmwdv79Qads.original.fullsize.png
[3.16]: https://cdn.mathpix.com/snip/images/UpBX8fY7X7N3siKiY_35XFxblrzXaf55ywk9uNjij1Q.original.fullsize.png
[3.17]: https://cdn.mathpix.com/snip/images/6HIRX6k3L61rBxaTrVIhpgVM1a5FpIbaU7NlfpQwkYk.original.fullsize.png
[3.18]: https://cdn.mathpix.com/snip/images/U29pfayF2g0iVmQCUISOAbfXUT1AHCpsRtcESQ7209k.original.fullsize.png
[3.19]: https://cdn.mathpix.com/snip/images/p3AqIByOJGZ3V_uM_4Sa3VejUi3w53V0emMterna21M.original.fullsize.png
[3.20]: https://cdn.mathpix.com/snip/images/Iq9a7eLhn6sfgRPlZlCssc4GNOS1QtxWHl_-96kN_nQ.original.fullsize.png
[3.21]: https://cdn.mathpix.com/snip/images/1ddIo6RqUxZ_BjXkKY1OjT2x80_Xv_CThFmyDdNPT4s.original.fullsize.png
[3.22]: https://cdn.mathpix.com/snip/images/HYSG57Y-nXuDk52lPxZfJSbQW-a9OiJy6rBhWBCmvUM.original.fullsize.png
[3.23]: https://cdn.mathpix.com/snip/images/Npp44DxBjMFpC4P2VIK482_oW-3WqPK14RQysl7dB1A.original.fullsize.png
[3.24]: https://cdn.mathpix.com/snip/images/YWDmcDLhpkxT5haY951lihS8fBrJDbkV_-RrWceErFM.original.fullsize.png
[3.25]: https://cdn.mathpix.com/snip/images/P7IILJ6-XQ-7K30wSyf1KXRqCTVID6k3JsCLoefSZhw.original.fullsize.png
[3.26]: https://cdn.mathpix.com/snip/images/HhkyTTr5_fiijqLN9MVba6t6bUTk5iETcCt83A3XQWk.original.fullsize.png
[3.27]: https://cdn.mathpix.com/snip/images/e7DAEI_aZsuCqQX9j4seaBRl_pXVE_nz92vUNPvWfsQ.original.fullsize.png
[3.28]: https://cdn.mathpix.com/snip/images/McFRGnvYT5SoteEP7Ipq2KF7k4QXWEZ2WgLmKNnXtWo.original.fullsize.png
[3.29]: https://cdn.mathpix.com/snip/images/QfAsNgL8sHbgV5PesGL-pBDps6CLFtoYh381WEj-Ckw.original.fullsize.png
[3.30]: https://cdn.mathpix.com/snip/images/YT_zxM_bWUw65ssJvDIUzm_BFcsJANz29gFS3PUMbN4.original.fullsize.png
[3.31]: https://cdn.mathpix.com/snip/images/Wzzyi14SqplIPdhs4RbCk2hxv7TrI6iTonPeN9YPCx0.original.fullsize.png
[3.32]: https://cdn.mathpix.com/snip/images/eza6lKfqH1yWyym2aB6ZOYlZ5H13ilJ3izsV2iDxtZQ.original.fullsize.png
[3.33]: https://cdn.mathpix.com/snip/images/Dv3BxdtFhzWihhGKxjThSYzF6MXD4g9QBnH-Rgzd48A.original.fullsize.png
[4.1]: https://cdn.mathpix.com/snip/images/l86CI2-RLpx_9Gs5cOxGeLD4AM_YfsAKjOox_S5yHK4.original.fullsize.png
[4.2]: https://cdn.mathpix.com/snip/images/SV4M9qExzYawOOjeV0m8JlQeis5I-rbhyntEFoRDq6c.original.fullsize.png
[5.1]: https://cdn.mathpix.com/snip/images/O_gnTovzOz0q0cWVWRU1t2gWKJXdjupFXuZDtoG2Z3I.original.fullsize.png
[5.2]: https://cdn.mathpix.com/snip/images/duV0oYM5w1YTFezH0LkcfH6goOlz6Gv_xHjTgxyneIQ.original.fullsize.png
[5.3]: https://cdn.mathpix.com/snip/images/QyYj7C7OUPvWBj3EyiNFg6bPIfPF-KQEmQ6fiD2xBas.original.fullsize.png
[5.4]: https://cdn.mathpix.com/snip/images/RGN5ds1ZVDdKLP3m62hG18XBuI-IeLAftYolpdjZMyE.original.fullsize.png
[5.4]: https://cdn.mathpix.com/snip/images/a-QXvHTPova4RR-pO_DphWHyTyC2bkZlv4Ii-qTDR7A.original.fullsize.png
[5.6]: https://cdn.mathpix.com/snip/images/gObfmEZAfgtiEBIjwuepnlAGWr7H9Nm1VRx9pyy0VRI.original.fullsize.png
[5.7]: https://cdn.mathpix.com/snip/images/GxW00vDuxzrWA9mQ1J1o7efugbmWybDZqCa-48MMXqE.original.fullsize.png
[5.8]: https://cdn.mathpix.com/snip/images/YI9nYwjtS6lc8XTgIPFf-ikFDv4AQkBByi3MGdQ97nQ.original.fullsize.png
[5.9]: https://cdn.mathpix.com/snip/images/u7Eu7VpMp7HSqQ26y9_GNhSWGP0q27ajbB3cf1x0KUw.original.fullsize.png
[5.10]: https://cdn.mathpix.com/snip/images/tuDaGho2QhJlyEPPjGM0S7pmKggSPJo3Vx1QAXoVeOI.original.fullsize.png
[5.11]: https://cdn.mathpix.com/snip/images/3iAN5b8h2v7xZaRJPM35uftmlk8qtc3a_xBRwWGQw94.original.fullsize.png
[5.12]: https://cdn.mathpix.com/snip/images/n1FIsfqKO_OV9KjfSq8lvKyUuys_Gk2yYIX_ZRNvbM0.original.fullsize.png
[5.13]: https://cdn.mathpix.com/snip/images/GuFUMvF-vk676WCfwRwzkYjw01KDbZdOF0qM__SKsXQ.original.fullsize.png
[5.14]: https://cdn.mathpix.com/snip/images/dzS2DIUmdAytT7WxrjbkaiHL4ODQAfmg82ORj_-b4lI.original.fullsize.png
[6.1]: https://cdn.mathpix.com/snip/images/9qQaxbPX5cOmF3JcarP5MR3nLStz_Y7QePDQGxMCKw0.original.fullsize.png
[6.2]: https://cdn.mathpix.com/snip/images/vGBdLKyG67S2Bfd0jmr639qpgn3PpGmyYePUlAAft4g.original.fullsize.png
[6.3]: https://cdn.mathpix.com/snip/images/ECiz3Auh2-uaDeACYO_Ukui9Owa9s300mUXGqAEHcE0.original.fullsize.png
[6.4]: https://cdn.mathpix.com/snip/images/Xfmi8o5j1jzlxhDRAac9ZZUL7MnRocL7YsRDp5Tk430.original.fullsize.png
[6.5]: https://cdn.mathpix.com/snip/images/SztkRDtNK7mGtMxRA8Us7PMpzH5dasewfJA1vBX-FCQ.original.fullsize.png
[7.1]: https://cdn.mathpix.com/snip/images/Qb8YNyjKbXGhONWGLGWgBF7pk9Q3sHjZ4YJbKBnt_4M.original.fullsize.png
[7.2]: https://cdn.mathpix.com/snip/images/sTzp3lPMfewZD2fe4R3ZsZ3T9Bw1jXAjgvQfJ8W8gbQ.original.fullsize.png
[7.3]: https://cdn.mathpix.com/snip/images/oqnus-aKzdvkqcQOF6WIUBfCXjQuhVs51GMHsJUrO2g.original.fullsize.png
[7.4]: https://cdn.mathpix.com/snip/images/3Qz48ktVIoLi018hdJ9QdhTKqUFeqoZVJDulmqtqedc.original.fullsize.png
[7.5]: https://cdn.mathpix.com/snip/images/TKbrdyBRxTVLjPJ4I46bk9kXSQNlPfLO68wfBbaINqU.original.fullsize.png
[7.6]: https://cdn.mathpix.com/snip/images/b2tX39SvDuCaUQJG8O5QBm_YTlyRnTvH5bB_6AKPwbw.original.fullsize.png
[7.7]: https://cdn.mathpix.com/snip/images/q0aQrX4M23p3b4SrcFVY2Dryc9RmocJjAIvaL7jf8fM.original.fullsize.png
[7.8]: https://cdn.mathpix.com/snip/images/mrzOOnz5nSifG2LwrWNx932s1kVcE9ognnMe-3CiTU4.original.fullsize.png
[7.9]: https://cdn.mathpix.com/snip/images/ZD854yMPmhmKsPBXPXA0QWl68-GCoCApXPNqYDqTmfg.original.fullsize.png
[7.10]: https://cdn.mathpix.com/snip/images/TAJkJkmCl2mz42vSmWXYyTC95_XK1HgmuePISO_7-Qk.original.fullsize.png
[7.11]: https://cdn.mathpix.com/snip/images/jtjhIm9tuOkSS8Ks6qr81nA2vx79jYB2SXxWwNh-DFQ.original.fullsize.png
[7.12]: https://cdn.mathpix.com/snip/images/0ZbZqBe1-4CSNUGLLfogfAo0M3GlDHAZjg-lfCz4d-I.original.fullsize.png
[7.13]: https://cdn.mathpix.com/snip/images/QUfNl38TKy8aqFO6VIrCSTY3RdIV_ACyU-Yqkthzc0A.original.fullsize.png
[7.14]: https://cdn.mathpix.com/snip/images/6AAJP4dRIFwQHuxxNJsegibfC1Le5JdHFNPLkTps6c4.original.fullsize.png
[7.15]: https://cdn.mathpix.com/snip/images/ripNRyGH5q6oiyYDT0QJ4Me1ap6PrGPCHywtrjQU9gA.original.fullsize.png
[7.16]: https://cdn.mathpix.com/snip/images/x0pyQRPTL9zUwtGn2bTTKzi1KpIU-vuV_c50RGFd2WM.original.fullsize.png
[7.17]: https://cdn.mathpix.com/snip/images/ASUA6KdFQ_67P4b9TzFuGYBkFyIMLohfmv8LQIDWxjk.original.fullsize.png
[7.18]: https://cdn.mathpix.com/snip/images/ZjC_LelowFzs6Wklg6ptYVmGpsBOo_65V-9fr5VuqQU.original.fullsize.png
[7.19]: https://cdn.mathpix.com/snip/images/7p8wqQu_FLQ-XQtVe8Ha9jnbsDJuB-gw59-JQYtNeqA.original.fullsize.png
[7.20]: https://cdn.mathpix.com/snip/images/1maE-VMGmE-wq6ag7bAERsj35Mk_yipGYYIjHxXNZRI.original.fullsize.png
[7.21]: https://cdn.mathpix.com/snip/images/MH5Z_RVONBx7XEtCVkXz-kNnM0U9mhFFVWJBm1WNj7k.original.fullsize.png
[7.22]: https://cdn.mathpix.com/snip/images/WxnPdncoL5bje-dq5rFiMVA3UQxK7mUgm8xSOeimyaA.original.fullsize.png
[7.23]: https://cdn.mathpix.com/snip/images/rs0jlr4oIkYfaLWKP398_NnqsY9RC8lRcWlwWMeZ2Gc.original.fullsize.png
[7.24]: https://cdn.mathpix.com/snip/images/dpSiCdTVlBH7QWA5YoZx4v1HCOEKRXCYKW-19CX8zFQ.original.fullsize.png
[7.25]: https://cdn.mathpix.com/snip/images/EDpbHXcaOJ3rXjXzN7rkXj8GOXKla4DgcHZWEoqBI0Y.original.fullsize.png
[7.26]: https://cdn.mathpix.com/snip/images/DAE8awruheNIb-rnB6eFe51OHKiNtaMZrIFBQcN3V6o.original.fullsize.png
[7.27]: https://cdn.mathpix.com/snip/images/bxOPspR2g1WXa3UxF7U0Kg1k0DjhplQ8DhqmGmX9Z44.original.fullsize.png
[7.28]: https://cdn.mathpix.com/snip/images/shMmgUEVKjq2ai3Uc8Fri7SoCLfGdIHL2LndTS8nAf4.original.fullsize.png
[7.29]: https://cdn.mathpix.com/snip/images/wg2lcjK9Vc1HPdpLy_E-b_GjbS4hpG-W4Fbikld0iZw.original.fullsize.png
[7.30]: https://cdn.mathpix.com/snip/images/c34pDBwCti2r9OqSxOrzZAdWs30ucC9r6dv9iv5kFaA.original.fullsize.png
[7.31]: https://cdn.mathpix.com/snip/images/L8UBca_kjRiifZSVjwXRiFwe5y8z2eOUxcYG3wuFasU.original.fullsize.png
[7.32]: https://cdn.mathpix.com/snip/images/k4LcvwsaR5yT9VJWDhNjvAHvG3KJwKTKJHdwDe42uEA.original.fullsize.png
[7.33]: https://cdn.mathpix.com/snip/images/FvOq_M3Pi_hix87egRIRuECtgz8xhlsYKRfFIqk-4ug.original.fullsize.png
[7.34]: https://cdn.mathpix.com/snip/images/6m-ix9Gokqr5exI1ZTxQx6iolQlVGoHWCjoZdBAB_AI.original.fullsize.png
[7.35]: https://cdn.mathpix.com/snip/images/q1BYTfojyYjUK-MOWlBUKdRp9qS6xjMWUdNMh9G9P4Q.original.fullsize.png
[7.36]: https://cdn.mathpix.com/snip/images/yMeft8RYj6gPLcvx1gP4QOMk7p_-BayDdAcb8YzGnXY.original.fullsize.png
[7.38]: https://cdn.mathpix.com/snip/images/30ipLrPXCvXPY-cPqc3zEqRrGDjZS46gUw98m2Kl6oo.original.fullsize.png
[7.39]: https://cdn.mathpix.com/snip/images/ZZVJTLJkF-5FF1BJ9H33uo_xBiZcM4n1opg0rKWQJC8.original.fullsize.png
[7.40]: https://cdn.mathpix.com/snip/images/XBgAlza4OhzyU4X_rQW8ntpg276EIVMNnvh0b6mFHe4.original.fullsize.png
[7.41]: https://cdn.mathpix.com/snip/images/LIqLjCL3WbTnzmMqrMdVhRqQhZKE4U8u-FXeSuGDcBc.original.fullsize.png
[7.42]: https://cdn.mathpix.com/snip/images/dT047so4Hp7zVFe8Ne3dLM2cHRr_zNEx9Ab5dpQb4RY.original.fullsize.png
[7.43]: https://cdn.mathpix.com/snip/images/v7B5uu0Di_dVsBKiTdqUP031ngJhMF_iPmlfdT4G-Pk.original.fullsize.png
[7.44]: https://cdn.mathpix.com/snip/images/jhRLEjpTMtkXTkO8qTVN-5WnxpoD8_XqG2AQR-c-Y9w.original.fullsize.png
[7.45]: https://cdn.mathpix.com/snip/images/TvjmUi-EtE2d5GOhizSdZgFPOTUSjCSEKYEjuvQrL0M.original.fullsize.png
[7.46]: https://cdn.mathpix.com/snip/images/4xPzplNZ6zDxfSVw4_IZfh6YdaI8mMS7qZhqZ3lGqnY.original.fullsize.png
[7.47]: https://cdn.mathpix.com/snip/images/wNTmQ3BS2Z9RJruIap9NqflBZdzRszAZ4T9XfWKGnK8.original.fullsize.png
[7.48]: https://cdn.mathpix.com/snip/images/_rekt-eo16hYb-JJRv706mzDb9EfZN9IZtElFuVlkUk.original.fullsize.png
[7.49]: https://cdn.mathpix.com/snip/images/X2qPhMs-b3ygdc1J8vs7tUmrC9XXK4AsKwZwdlnHlyI.original.fullsize.png
[7.50]: https://cdn.mathpix.com/snip/images/8m2HQhFToZA3aQ8e8qE7Z__gkIYfN8DuZao_y6TeYQ4.original.fullsize.png
[7.51]: https://cdn.mathpix.com/snip/images/oeET2B6hjjx1iCn3O7vQ1-SRgRlg6ac7TXuKCB1_RTc.original.fullsize.png
[7.52]: https://cdn.mathpix.com/snip/images/zjavh1L9WTvemHgjScUe0xMb_4KTopzdgcURo9CufrQ.original.fullsize.png
[7.53]: https://cdn.mathpix.com/snip/images/PzLVm7SFYTAd3zNWTAOJvnlbNsa1MrGI5vajBRL0okg.original.fullsize.png
[7.54]: https://cdn.mathpix.com/snip/images/wWxe7dkarz8BnN7ERmmEiB7z9gXIgwNfiW15u-nTddU.original.fullsize.png
[7.55]: https://cdn.mathpix.com/snip/images/UFuFO7xiMC53xrYixY5EuetQe2hSU2CXUTYWs3SrAYw.original.fullsize.png
[7.56]: https://cdn.mathpix.com/snip/images/CsNQGtVIUqar4QIcuIxOXLIRcpSolrHYa4-vzTlRpHQ.original.fullsize.png
[7.57]: https://cdn.mathpix.com/snip/images/djk1Kw-eGO8QcOLLx5PB2MOubRz8TN1H_LswNp8Wh00.original.fullsize.png
[7.58]: https://cdn.mathpix.com/snip/images/tDiGhgkYmnbEtXce41o07_7TwEXT5Sh2xWEA8AyK5HE.original.fullsize.png
[7.59]: https://cdn.mathpix.com/snip/images/hxleGlRPS1N8Efr7-tJriwZIMaBLU-L5ouYEV_Ue88A.original.fullsize.png
[7.60]: https://cdn.mathpix.com/snip/images/DD8k7P4DTBQV22tUC8lN5K6_3HL7wSTutGnmyKCawl8.original.fullsize.png
[7.61]: https://cdn.mathpix.com/snip/images/6JcYF3m7gVIMixcHs3GoiW5lU4s7LTQkLVZjYLeFlmU.original.fullsize.png
[7.62]: https://cdn.mathpix.com/snip/images/tIra-kT8_1BSrpHae_dgtzDKWwAQnj1QWxAxjp7KKh0.original.fullsize.png
[7.63]: https://cdn.mathpix.com/snip/images/hpV5Y6TRBrOqFHfda_1Hn4P8nDA8V5QhlQo9bw29_ZE.original.fullsize.png
[7.64]: https://cdn.mathpix.com/snip/images/cQNIwALoiET0HAfXct7UVW1x0YE6auIi7U-l0Br4v6U.original.fullsize.png
[7.65]: https://cdn.mathpix.com/snip/images/5pjAs37kSlLzC3zT29F8eguUQB1qy5D1YjRpVj-POkQ.original.fullsize.png
[7.66]: https://cdn.mathpix.com/snip/images/9hhIb20Jc3V9YOHRFyPI-x-YTP11R7yVmzR-e_SXLRo.original.fullsize.png
[7.67]: https://cdn.mathpix.com/snip/images/fhStNtD0VQEoTbtxvHAy7VT2EmWOVpP8z0OpPm4k9ZE.original.fullsize.png
[7.68]: https://cdn.mathpix.com/snip/images/D8uWmGb-jBbl0nAebxhWhiqfM4rJ2Fl-J8kYr45tjZ8.original.fullsize.png
[7.69]: https://cdn.mathpix.com/snip/images/St-p3bU9k552AY0a4jRzjwkAZEH85JSoejk5n5kE_UM.original.fullsize.png
[7.70]: https://cdn.mathpix.com/snip/images/EsywhpjYv9GE2Q4KzODENjaTF-QcDf2vuFWH5Di3w5M.original.fullsize.png
[7.71]: https://cdn.mathpix.com/snip/images/L186xsOOPmlW6cgD4NwTJtAzuvTrRAnEsbEGJyvLlbg.original.fullsize.png
[7.72]: https://cdn.mathpix.com/snip/images/9DEc7uwlYFE68JMzl1Qm5vIFAMOii1O8DWv5OXO37Mw.original.fullsize.png
[7.73]: https://cdn.mathpix.com/snip/images/j3NXt2dQCXKpLK3X7Nmgbb2WmRHB7mx_3eycNtsloBE.original.fullsize.png
[7.74]: https://cdn.mathpix.com/snip/images/CRJmBngpbYhFwp78VbrF1yrNynelJCaXlnYoA4WZUjA.original.fullsize.png
[7.75]: https://cdn.mathpix.com/snip/images/Yx3YZMvSHCRXgmA26y5z51epKVdovGfQByVi0_VSpzo.original.fullsize.png
[7.76]: https://cdn.mathpix.com/snip/images/mc0ehT6-RCci2cglH5HOCv6vsDf-fjg2hQ41BWP8bsg.original.fullsize.png
[7.77]: https://cdn.mathpix.com/snip/images/zrOcxxYaR32TxflxPMClMyP1cDVbxqXkKUcMhEbRykw.original.fullsize.png
[7.78]: https://cdn.mathpix.com/snip/images/ksIesBI_fKiFmMTqJaAaGkCEt9ZYEooYP4ks9EIrUGM.original.fullsize.png
[8.1]: https://cdn.mathpix.com/snip/images/usXB97wg5g0MYVdt6mtfyCLMp6sElI89IqRct2K3Gzw.original.fullsize.png
[8.2]: https://cdn.mathpix.com/snip/images/ZrZbNXvpzFywkh3hENEfp0GxClFS-2WxJi53fyRJHXs.original.fullsize.png
[8.3]: https://cdn.mathpix.com/snip/images/vq_fAcIT7xC2OAHbFKIXurSNIfmVGRODUMQeOUHKGAQ.original.fullsize.png
[8.4]: https://cdn.mathpix.com/snip/images/65s_9w9GL0BO-C-YLJAITbfdc8ZF5D-_Qfi3p-vD4I8.original.fullsize.png
[8.5]: https://cdn.mathpix.com/snip/images/STN45Nx1Ky9N3bddZoiRbR_LK0MyBRYNdacbBbvRow8.original.fullsize.png
[8.6]: https://cdn.mathpix.com/snip/images/CFIbvFWaJ5ZKJIEEEcCl6MXxEe35B09tJLP876Sw3i0.original.fullsize.png
[8.7]: https://cdn.mathpix.com/snip/images/IuFj6-2j56goexLOmev729mD8VdIHv_18npLe22RwlQ.original.fullsize.png
[8.8]: https://cdn.mathpix.com/snip/images/IPcWtWIToIXZJqkU0PVwezDQ7OBlVRVC36b_gVrfkQg.original.fullsize.png
[8.9]: https://cdn.mathpix.com/snip/images/gD6TjrvZ1FtM5SJI35hXorLRAD53TSfGHOgl7C__Srw.original.fullsize.png
[8.10]: https://cdn.mathpix.com/snip/images/yVxCaXFihG4J4EDOdhEmc7PKJh2qHU38sOzhOjh06JI.original.fullsize.png
[8.11]: https://cdn.mathpix.com/snip/images/OoBW9iZdXL2j7BkwCWNEnDLcCZOE-fV0MwACS7NfAvE.original.fullsize.png
[9.1]: https://cdn.mathpix.com/snip/images/oU-Lkeu4m1MigRgh_Py7c-RLpIvv5aMO1-2lMfHqTRw.original.fullsize.png
[9.2]: https://cdn.mathpix.com/snip/images/nUfJfohGnnHyRE-abVZ_6r15JeUcYH4abPwx2Bi7onU.original.fullsize.png
[9.3]: https://cdn.mathpix.com/snip/images/XRcabOF5rZ0lmVkIHbzUIQyhkBTQ9tDg7Z6muKrBz0s.original.fullsize.png
[9.4]: https://cdn.mathpix.com/snip/images/yndIzdeZWGWbVxRNhcgEnkcFe9ZObN2y4ASStxGZObQ.original.fullsize.png
[9.5]: https://cdn.mathpix.com/snip/images/McbPnoBIOP15eplXp0DDUc3ai3l4uppmNciteT4pBQ0.original.fullsize.png
[9.6]: https://cdn.mathpix.com/snip/images/_QLYFq76Qa91j1qz9Oip1uZwjTWxxM_hwckhRBoUSdo.original.fullsize.png
[9.7]: https://cdn.mathpix.com/snip/images/c5QSNBHadExARAn1UXeV0aFoFBAHPwzAs1_MSfLX5AA.original.fullsize.png
[9.8]: https://cdn.mathpix.com/snip/images/GFf_U1Ej_KEQ0wRI_X4-erxstLVG621bh0Bh4brTTBE.original.fullsize.png
[9.9]: https://cdn.mathpix.com/snip/images/ycqE41YgNHiSdLMp3ETFVunq-cf028upPFfcTafwpic.original.fullsize.png
[9.10]: https://cdn.mathpix.com/snip/images/UdkQdj_Kf4wM04wHS39y1Vx7A9D9-kLU29i1gLm-xaM.original.fullsize.png
[9.11]: https://cdn.mathpix.com/snip/images/EvrDQX45dk4VoO9XXRxBIg19jVwB7_7oc9zVAsxzLnM.original.fullsize.png
[9.12]: https://cdn.mathpix.com/snip/images/Pxmv2T6mR1-wQNjv-Ge8aTeAdb-IjrejbqUiL_4DG3c.original.fullsize.png
[9.13]: https://cdn.mathpix.com/snip/images/WG_KLDYPwYVWkN_vcv1RiIJRRYQUPUEki74ZBtOBTK0.original.fullsize.png
[9.14]: https://cdn.mathpix.com/snip/images/hzKj_1vW0iEwJce3js279-mgg6cz-fO2AA-S2Kv2myM.original.fullsize.png
[9.15]: https://cdn.mathpix.com/snip/images/gz1UNtP-P0edILOjAtRZiP-6J_0PP7XM0JZD8zZdesg.original.fullsize.png
[9.16]: https://cdn.mathpix.com/snip/images/TFudSRKmY4XmMKhvRnBmtofFefhgbH12ingffYz-0oA.original.fullsize.png
[9.17]: https://cdn.mathpix.com/snip/images/b-W3tyTDkPLaui5M3-xMCWn5TqoiQGqG_Fdp23cVItM.original.fullsize.png
[9.18]: https://cdn.mathpix.com/snip/images/gQ62cOo-mImla9og2LyH76VQysMhqZhYn3ckT-aM36A.original.fullsize.png
[9.19]: https://cdn.mathpix.com/snip/images/6DCSOCV7s1OLLgA8Vat2-r_lsLBuVDaaDvg0abRPu6U.original.fullsize.png
[9.20]: https://cdn.mathpix.com/snip/images/NNduBimvTXvCAtlmdmTaZ6MjpCM6BLeyl9UwIcGKAoU.original.fullsize.png
[9.21]: https://cdn.mathpix.com/snip/images/wewP0l6SszjX20hGb45Y57w6MOkbhMeyrtbzMQybqmM.original.fullsize.png
[9.22]: https://cdn.mathpix.com/snip/images/CTE-xYxyAyVL0XQmhW2w039onOKU2-W6zbELYlfg5z8.original.fullsize.png
[9.23]: https://cdn.mathpix.com/snip/images/tIHC8vyIGaRKAhT1yD2tJ6QQJHT95470IevhoNc1dXs.original.fullsize.png
[9.24]: https://cdn.mathpix.com/snip/images/pVb1nY0eqQMwvjPESoGPpZVOD0zaFq2uyiJI38JRIzk.original.fullsize.png
[9.25]: https://cdn.mathpix.com/snip/images/B9FU_Y8JTbgT14n6PEHTLPKVETpKOJf2aG3dfZhfAYA.original.fullsize.png
[9.26]: https://cdn.mathpix.com/snip/images/AG3YCmFQ_8nwMJgN4qVBUCYOptpKq4ti6LAvxTactHM.original.fullsize.png
[9.27]: https://cdn.mathpix.com/snip/images/kFQ7iiDOMlA_I2S4_Bn8UMx5_ndCpxAKTgzHBHK_MlE.original.fullsize.png
[9.28]: https://cdn.mathpix.com/snip/images/tsk9CHAL54doPNpsJq7_UG3i-w13gte_Jz5yWGztQOw.original.fullsize.png
[9.29]: https://cdn.mathpix.com/snip/images/CK_nK5BNWyyADcdYKY2_4INVimC7B2VIgvYNm9za1zg.original.fullsize.png
[9.30]: https://cdn.mathpix.com/snip/images/J7O8DwDNZn9C9ZZb5BdJlyA5Q-roVzCQENZ0ShNJtRc.original.fullsize.png
[9.31]: https://cdn.mathpix.com/snip/images/NLaCt16pMGtQL8JgVRPn3-RL_gpT1VjcU8JpwICa55s.original.fullsize.png
[9.32]: https://cdn.mathpix.com/snip/images/kjM4RtZMXe-Bt3HkBWfcCfl-gHhEMAZ2Uqq66kt8H6s.original.fullsize.png
[9.33]: https://cdn.mathpix.com/snip/images/qgumB1DK_uU7QMpifuLEOMfUUTSHlj6Waw0TcJth0gg.original.fullsize.png
[10.1]: https://cdn.mathpix.com/snip/images/OpxrnVwNeDXvBks7Y76jhvu4K9bCtH0--vD_61gQJac.original.fullsize.png
[10.2]: https://cdn.mathpix.com/snip/images/x99zIhZz8QrzjYklcb-KkEBZJiTtSxngMoSPSwgCHR0.original.fullsize.png
[10.3]: https://cdn.mathpix.com/snip/images/CsXQ_Zlgh6ki9QtyhT1vrjQ903jpPRVI1qWvqtWbJL0.original.fullsize.png
[10.4]: https://cdn.mathpix.com/snip/images/7szORDtcImSATkwx8Cn-HEV4d5weKE02OojUzPqjTFg.original.fullsize.png
[10.5]: https://cdn.mathpix.com/snip/images/IC_pJM3rnRli4a__Yj2OJKadkMcXTk68jgFyJ1NtVOg.original.fullsize.png
[10.6]: https://cdn.mathpix.com/snip/images/DcMktGdtYmQOtYbbPXsPClNS1G0nagIlxLKfjSM5Aoo.original.fullsize.png
[10.7]: https://cdn.mathpix.com/snip/images/8or8N-VKRaxsaLpTxxWzs_mEwh07zPN3ErtlrlZPr4E.original.fullsize.png
[10.8]: https://cdn.mathpix.com/snip/images/Zea1QhTiPPcIECWL8-uNv8FWvlSQKIdMYn6fHgnoHrk.original.fullsize.png
[10.9]: https://cdn.mathpix.com/snip/images/ymwUafacv9_0lbKWM77LHG6dXR_Kq6Q4MaoO1wsHQks.original.fullsize.png
[10.10]: https://cdn.mathpix.com/snip/images/4Dx2XolfMc9L0TNJngV36J4UzLiuaMTyLbS3Ve_Gx1c.original.fullsize.png
[10.11]: https://cdn.mathpix.com/snip/images/IYjA3IgtiUfsrGQwc9i_ahnH67xl_I6wG_evCU5VmBc.original.fullsize.png
[10.12]: https://cdn.mathpix.com/snip/images/t4Rhi4VJPx7kkcnv9lZC4cKcB0wRFoiaQHEVoN_pDnE.original.fullsize.png




[表5.1]: https://cdn.mathpix.com/snip/images/8f-55Tm6G94G_EgNHsEgVO4Ia0R6YwrOzUQaLTqheRI.original.fullsize.png
[表5.2]: https://cdn.mathpix.com/snip/images/N-JzVy9zMipV5s0yN6QNtI8EBAntiKhWl3YaEvbuus4.original.fullsize.png
[表5.3]: https://cdn.mathpix.com/snip/images/QDbaonP0OB9Ndo5t-_pLgWgYvHxMruegDRrkWJgTCBw.original.fullsize.png
[表7.1]: https://cdn.mathpix.com/snip/images/T2zhXSEBFiqgnBw7IKxdyHsC9M4nM2vjenwyAgiSqbI.original.fullsize.png
[表7.2]: https://cdn.mathpix.com/snip/images/UHSyErgxB-Y1mqjbZve9jXDiTHh_FNPofzajk3HxeCs.original.fullsize.png
[表7.3]: https://cdn.mathpix.com/snip/images/KGqfTLSZITdxfZGbH5BQ1slJyGnVqb8QJKeViydzBLc.original.fullsize.png
[表7.4]: https://cdn.mathpix.com/snip/images/uYgQSTBmW8yG_2-5SW9GY3cp9ADYwJYQ1OoWSddVw70.original.fullsize.png
[表9.1]: https://cdn.mathpix.com/snip/images/TqTcb9tK90BWdAQCzz1tromxRTr6Rj1MuY-gXAVce0A.original.fullsize.png
[表9.2]: https://cdn.mathpix.com/snip/images/ADjOQfEVW2BKJOzGhBEVL7jE3N37USeisBUzi_0awmo.original.fullsize.png
[表9.3]: https://cdn.mathpix.com/snip/images/bJTUNrMl7A85o9EunW3a0sMHXJJ-xmpDqI1FLdR84LA.original.fullsize.png
[表9.4]: https://cdn.mathpix.com/snip/images/xh8zdRGbT3BP-FdSiYB_xPyyj3Hoa8RLDDbhy2NeY2Y.original.fullsize.png



\begin{tabular}{ll}
\hline Method Name & Operation \\
\hline Size & Returns Size of the vector. \\
Size1 & Return number of rows of matrix. \\
Size2 & Return number of columns of matrix. \\
Resize & Resizes the vector \\
Resize & Resizes the matrix \\
Copy & $X \rightarrow Y$ \\
Copy & $A \rightarrow B$ \\
Dot & $X \cdot Y$ \\
TwoNorm & $\|X\|_{2}$ \\
Mult & $A \cdot X \rightarrow Y$ \\
TransposeMult & $A^{T} \cdot X \rightarrow Y$ \\
RowDot & $A_{i} \cdot X$ \\
ScaleAndAdd & $\alpha X+\beta Y \rightarrow Z$ \\
ScaleAndAdd & $\alpha X+\beta Y \rightarrow Y$ \\
GraphDegree & Number of nonzeros in given row. \\
GraphNeighbors & Columns of nonzeros in given row. \\
\hline
\end{tabular}
Table 5.1: Interface of Space



\begin{tabular}{ll}
\hline Method & Information \\
\hline Dimension & Dimension of the geometry \\
WorkingSpaceDimension & Dimension of space which geometry is definied \\
LocalSpaceDimension & Geometries local dimension \\
PointsNumber & Number of points creating geometry \\
Length & Length of 1 dimensional geometries \\
Area & Area of 2 dimensional geometries \\
Volume & Volume of 3 dimensional geometries \\
DomainSize & Length, area, or volume depending to dimension \\
Center & Center point of geometry \\
Points & Geometries' points \\
pGetPoint & Pointer to $i$ -th point of geometry \\
GetPoint & $i$ -th point of geometry \\
EdgesNumber & number of edges of this geometry \\
Edges & Edges of geometry \\
pGetEdge & Pointer to $i$ -th edge of geometry \\
GetEdge & $i$ -th edge of geometry \\
IsSymmetric & True if geometry is symmetric \\
\hline
\end{tabular}
Table 5.2: Geometry methods implementing geometrical operations.


\begin{tabular}{ll}
\hline Method & Information \\
\hline HasIntegrationMethod & True if implements the integration method \\
IntegrationPointsNumber & Number of integration points \\
IntegrationPoints & Array of integration points \\
GlobalCoordinates & Global coordinates related to given local one \\
Jacobian & Jacobian matrix J \\
DeterminantOf Jacobian & Determinant of jacobian $|\mathbf{J}|$ \\
Inverse0fJacobian & Inverse of jacobian $\mathbf{J}^{-1}$ \\
ShapeFunctionsValues & shape functions' values in integration points \\
ShapeFunctionValue & Value of shape function $i$ in integration point $j$ \\
ShapeFunctionsLocalGradients & \\
ShapeFunctionLocalGradient & \\
ShapeFunctionsFirstDerivatives & \\
ShapeFunctionsSecondDerivatives & \\
ShapeFunctionsThirdDerivatives & \\
\hline
\end{tabular}
Table 5.3: Geometry methods providing finite element operations.


\begin{tabular}{ll}
\hline Method Name & Operation \\
\hline Number0fNodes & Returns the number of Nodes in the Mesh. \\
AddNode & Add given Node to its Nodes container. \\
pGetNode & Returns a pointer to the Node with the given identifier. \\
GetNode & Returns a reference to the Node with the given identifier. \\
RemoveNode & Removes the Node with given Id from the Mesh. \\
RemoveNode & Removes the given Node from the Mesh. \\
NodesBegin & Returns a Node iterator pointing to the beginning of the Nodes. \\
NodesEnd & Returns a Node iterator pointing to the end of the Nodes container. \\
Nodes & Returns the Nodes container. \\
pNodes & Returns a pointer to the Nodes container. \\
SetNodes & Sets the given container as is Nodes container. \\
NodesArray & Returns the internal array of Nodes. \\
\hline
\end{tabular}
Table 7.1: Interface of Mesh for accessing Nodes


\begin{tabular}{ll}
\hline Method Name & Operation \\
\hline Number0fProperties & Returns the number of properties in the Mesh. \\
AddProperties & Add given properties to its properties container. \\
pGetProperties & Returns a pointer to the properties with the given identifier. \\
GetProperties & Returns a reference to the properties with the given identifier. \\
RemoveProperties & Removes the properties with given Id from the Mesh. \\
RemoveProperties & Removes the given Properties from the Mesh. \\
PropertiesBegin & Returns the begin iterator of the properties container. \\
PropertiesEnd & Returns the end iterator of the properties container. \\
Properties & Returns the properties container. \\
pProperties & Returns a pointer to the properties container. \\
SetProperties & Sets the given container as is properties container. \\
PropertiesArray & Returns the internal array of properties. \\
\hline
\end{tabular}
Table 7.2: Interface of Mesh for accessing properties


\begin{tabular}{ll}
\hline Method Name & Operation \\
\hline Number0fElements & Returns the number of Elements in the Mesh. \\
AddElement & Add given Element to its Elements container. \\
pGetElement & Returns a pointer to the Element with the given identifier. \\
GetElement & Returns a reference to the Element with the given identifier. \\
RemoveElement & Removes the Element with given Id from the Mesh. \\
RemoveElement & Removes the given Element from the Mesh. \\
ElementsBegin & Returns the begin iterator of the Elements container. \\
ElementsEnd & Returns the end iterator of the Elements container. \\
Elements & Returns the Elements container. \\
pElements & Returns a pointer to the Elements container. \\
SetElements & Sets the given container as is Elements container. \\
ElementsArray & Returns the internal array of Elements. \\
\hline
\end{tabular}
Table 7.3: Interface of Mesh for accessing Elements


\begin{tabular}{ll}
\hline Method Name & Operation \\
\hline Number0fConditions & Returns the number of Conditions in the Mesh. \\
AddCondition & Add given Condition to its Conditions container. \\
pGetCondition & Returns a pointer to the Condition with the given identifier. \\
GetCondition & Returns a reference to the Condition with the given identifier. \\
RemoveCondition & Removes the Condition with given Id from the Mesh. \\
RemoveCondition & Removes the given Condition from the Mesh. \\
ConditionsBegin & Returns the begin iterator of the Conditions container. \\
ConditionsEnd & Returns the end iterator of the Conditions container. \\
Conditions & Returns the Conditions container. \\
pConditions & Returns a pointer to the Conditions container. \\
SetConditions & Sets the given container as is Conditions container. \\
ConditionsArray & Returns the internal array of Conditions. \\
\hline
\end{tabular}
Table 7.4: Interface of Mesh for accessing Conditions


\begin{tabular}{ll}
\hline Types & Concepts \\
\hline int & Id, Connectivity \\
double & Temperature \\
vector & Thermal flow \\
Matrix & Thermal conductivity \\
\hline
\end{tabular}
Table 9.1: Thermal application types and concepts


\begin{tabular}{ll}
\hline Name & Index \\
\hline TEMPERATURE & 0 \\
VELOCITY_X & 1 \\
VELOCITY_Y & 2 \\
THERMAL_FLOW & 3 \\
THERMAL_CONDUCTIVITY & 4 \\
\hline
\end{tabular}
Table 9.2: A sample part of lookup table


\begin{tabular}{ll}
\hline Token's Type & Examples \\
\hline INTEGER & 134610 \\
REAL & $0.234 .572 \mathrm{e}-4$ \\
IF & if \\
FOR & for \\
LPARENTHESES & ( \\
RPARENTHESES & ) \\
\hline
\end{tabular}
Table 9.3: Some typical tokens with example of matching sequences



\begin{tabular}{ll}
\hline Operator                      & Description\\
$! \mathrm{P}$      				 & Zero or One P\\
$* \mathrm{P}$      				 & One or more P\\
$+\mathrm{P}$       				 & One or more P\\
$\sim \mathrm{P}$   				 & Anything except P\\
$\mathrm{P} 1 \% \mathrm{P} 2$ 		 & One or more P1 separated by P2\\
$\mathrm{P} 1-\mathrm{P} 2$ 		 & P1 but not P2\\
$\mathrm{P} 1>>\mathrm{P} 2$		 & P1 followed by P2\\
$\mathrm{P} 1 \& \mathrm{P} 2$       & P1 and P2\\
$\mathrm{P} 1 \wedge \mathrm{P} 2$   & P1 or P2, but not both\\
$\mathrm{P}1 |  \mathrm{P} 2$        & P1 or P2\\
$\mathrm{P} 1  \& \& \mathrm{P} 2$ 	 & Synonym for P1 >> P2\\
$\mathrm{P}1 || \mathrm{P} 2$ 		 & P1 or P2 or P1 >> P2\\
\hline
\end{tabular}
Table 9.4: Spirit’s operators








\section{References}

[^1]: Ansys. http://www.ansys.com/default.asp.
[^2]: Bison. http://www.gnu.org/software/bison/.
[^3]: Boost library. http://www.boost.org/.
[^4]: Boost serialization library. http://www.boost.org/libs/serialization/doc/index.html.
[^5]: Boost spirit library. http://www.boost.org/libs/spirit/index.html.
[^6]: Flex. http://flex.sourceforge.net/.
[^7]: A quality tetrahedral mesh generator and three-dimensional delaunay triangulator (tetgen) programming interface. http://tetgen.berlios.de/library.html.
[^8]: Stlport. http://www.stlport.org/.
[^9]: Xparam library. http://xparam.sourceforge.net/.
[^10]: ABAQUS Inc. ABAQUS Scripting Reference Manual.
[^11]: ABAQUS Inc. ABAQUS Scripting User's Manual.
[^12]: A. V. Aho, R. Sethi, and J. D. Ullman. Compilers Priciples, Techniques, and Tools. AddisonWesley, 1986 .
[^13]: A. V. Aho and J. D. Ullman. Principles of Compiler Design. Addison-Wesley, 1978 .
[^14]: A. V. Aho, J. D. Ullman, and J. E. Hopcroft. Data Structures and Algorithms. AddisonWesley, 1983 .
[^15]: E. Anderson, Z. Bai, C. Bischof, S. Blackford, J. Demmel, J. Dongarra, J. D. Croz, A. Greenbaum, S. Hammarling, A. McKenney, and D. Sorensen. LAPACK Users' Guide. Society for Industrial and Applied Mathematics, Philadelphia, PA, USA, second edition, 1995 .
[^16]: A. W. Appel and M. Ginsburg. Modern Compiler Implementation in C. Cambridge University Press, 1999 .
[^17]: G. C. Archer. Object-Oriented Finite Element Analysis. PhD thesis, University of California at Berkeley, 1996 . 
[^18]: G. C. Archer, G. Fenves, and C. Thewalt. A new object-oriented finite element analysis program architecture. Computers $\&$ Structures, $70(1): 63-75,1999$.
[^19]: W. Bangerth. Using modern features of $\mathrm{C}++$ for adaptive finite element methods: Dimension-independent programming in deal.II. In M. Deville and R. Owens, editors, Proceedings of the 16th IMACS World Congress 2000, Lausanne, Switzerland, 2000, 2000. Document Sessions/118-1.
[^20]: W. Bangerth, R. Hartmann, and G. Kanschat. deal.II Differential Equations Analysis Library, Technical Reference. http: //www.dealii.org.
[^21]: W. Bangerth, R. Hartmann, and G. Kanschat. deal.II - a general purpose object oriented finite element library. Technical Report ISC-06-02-MATH, Institute for Scientific Computation, Texas A\&M University, 2006 .
[^22]: W. Bangerth and G. Kanschat. Concepts for object-oriented finite element software - the deal. II library. Preprint 99-43 (SFB 359), IWR Heidelberg, Oct. 1999 .
[^23]: K.-J. Bathe. Finite element procedures. Prentice-Hall, 1996 .
[^24]: C. Bishop. Neural Networks for Pattern Recognition. Oxford University Press, 1995 .
[^25]: A. Cardona, I. Klapka, and M. Geradin. Design of a new finite element programming environment. Engineering Computations, $11(4): 365-381,1994$.
[^26]: R. Codina. Pressure stability in fractional step finite element methods for incompressible flows. Journal of Computational Physics, 170:112140, 2001 .
[^27]: R. Codina. Stabilized finite element approximation of transient incompressible flows using orthogonal subscales. Computer Methods in Applied Mechanics and Engineering, $191(39): 4295$ 4321,2002
[^28]: H.-P. Company. Sgi standard template library (stl). http://www.sgi.com/tech/stl/.
[^29]: Coupled Problems 2007. Updated lagrangian formulation of a quasi-incompressible fluid element, Ibiza, Spain, $2007 .$
[^30]: P. Dadvand, R. Lopez, and E. Oñate. Artificial neural networks for the solution of inverse problems. In ERCOFTAC, 2006 .
[^31]: P. Dadvand, J. Mora, C. González, A. Arraez, P. Ubach, and E. Oñate. Kratos: An objectoriented environment for development of multi-physics analysis software. In Proceedings of the WCCM V Fifth World Congress on Computational Mechanics. WCCM V Fifth World Congress on Computational Mechanics, July 2002 .
[^32]: J. Donéa and A. Huerta. Finite Element Methods for Flow Problems. John Wiley and Sons, $2003 .$
[^33]: Y. Dubois-Pélerin and P. Pegon. Improving modularity in object-oriented finite element programming. Communications in Numerical Methods in Engineering., 13:193-198, 1997 .
[^34]: Y. Dubois-Pèlerin and T. Zimmermann. Object-oriented finite element programming: Iii. an efficient implementation in c++. Comput. Methods Appl. Mech. Eng., $108(1-2): 165-183$, $1993 .$ 
[^35]: Y. Dubois-Pèlerin, T. Zimmermann, and P. Bomme. Object-oriented finite element in programming: Ii. a prototype program in smalltalk. Comput. Methods Appl. Mech. Eng., $98(3): 361-397,1992 .$
[^36]: I. S. Duff, A. M. Erisman, and J. K. Reid. Direct methods for sparse matrices. Clarendon Press, New York, NY, USA, $1989 .$
[^37]: D. R. Edelson. Smart pointers: They're smart, but they're not pointers. Technical report, Santa Cruz, CA, USA, 1992 .
[^38]: D. Eyheramendy and T. Zimmermann. Object-oriented finite element programming: an interactive environment for symbolic derivations, application to an initial boundary value problem. Adv. Eng. Softw., $27(1-2): 3-10,1996$.
[^39]: D. Eyheramendy and T. Zimmermann. Object-oriented finite elements ii. a symbolic environment for automatic programming. Comput. Methods Appl. Mech. Eng., $132(3): 277-304(28)$, June 1996 .
[^40]: D. Eyheramendy and T. Zimmermann. Object-oriented finite elements iii. theory and application of automatic programming. Comput. Methods Appl. Mech. Eng., $154(1): 41-68(28)$, February 1998 .
[^41]: C. A. Felippa and T. L. Geers. Partitioned analysis of coupled mechanical systems. Engrg. Comput., 5:123-133, 1988 .
[^42]: C. A. Felippa, K. C. Park, and C. Farhat. Partitioned analysis of coupled mechanical systems. Computer Methods in Applied Mechanics and Engineering, 190:3247-3270, 2001 .
[^43]: J. R. A. Filho and P. R. B. Devloo. Object oriented programming in scientific computations : the beginning of a new era. Engineering computations, $8(1): 81-87,1991$.
[^44]: B. W. R. Forde, R. O. Foschi, and S. F. Stiemer. Object-oriented finite element analysis. Comp. Struct., $34(3): 355-374,1990 .$
[^45]: E. Gamma, R. Helm, R. Johnson, and J. Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison Wesley Professional, $1995 .$
[^46]: G. H. Gonnet and R. Baeza-Yates. Handbook of algorithms and data structures: in Pascal and $C$ (2nd ed.). Addison-Wesley Longman Publishing Co., Inc., Boston, MA, USA, $1991 .$
[^47]: S. Haykin. Neural Networks: A Comprehensive Fundation. Prentice Hall, $1994 .$
[^48]: K. Hornik, M. Stinchcombe, and H. White. Multilayer feedforward networks are universal approximators. Neural Netw., $2(5): 359-366,1989$.
[^49]: S. R. Idelson and E. Oñate. To mesh or not to mesh. that is the question. Computer methods in applied mechanics and engineering, 195:4681-4696, 2006.
[^50]: S. R. Idelson, E. Oñate, N. Calvo, and F. D. Pin. The meshless finite element method. International Journal for Numerical Methods in Engineering, $58: 893-912,2003 .$
[^51]: S. R. Idelson, E. Oñate, and F. D. Pin. The particle finite element method: a powerful tool to solve incompressible flows with free-surfaces and breaking waves. International Journal for Numerical Methods in Engineering, $61: 964-989,2004 .$ 
[^52]: A. Kirsch. An Introduction to the Mathematical Theory of Inverse Problems. Springer, 1996 .
[^53]: I. Klapka, A. Cardona, and M. Geradin. An object oriented implementation of the finite element method for coupled problems. Revue Europenne des lments Finis, $7(5): 469-504$, 1998 .
[^54]: I. Klapka, A. Cardona, and M. Geradin. Interpreter oofelie for pdes. In European Congress on Computational Methods in Applied Sciences and Engineering (ECCOMAS 2000), $2000 .$
[^55]: D. E. Knuth. The Art of Computer Programming: Sorting and Searching, volume 3. AddisonWesley, 2nd edition, 1998 .
[^56]: Laboratory for Computational Physics and Fluid Dynamics (LCP\&FD). FEFLO Project.
[^57]: A. Larese, R. Rossi, E. Oñate, and S. R. Idelson. Validation of the particle finite element method (pfem) for simulation of free surface flows. Submitted for publication to Engineering and Computation, $2007 .$
[^58]: J. Lindemann, O. Dahlblom, and G. Sandberg. Using corba middleware in finite element software. Future Generation Comp. Syst., $22(1-2): 158-193,2006$.
[^59]: R. Löhner. Applied CFD Techniques: An Introduction Based on Finite Element Methods. Wiley, 2001 .
[^60]: R. Lopez. Flood: An Open Source Neural Networks $C++$ Library. CIMNE.
[^61]: J. Lu, D. White, and W.-F. Chen. Applying object-oriented design to finite element programming. In SAC '93: Proceedings of the 1993 ACM/SIGAPP symposium on Applied computing, pages 424-429, New York, NY, USA, 1993. ACM Press.
[^62]: J. Lu, D. W. White, W.-F. Chen, and H. E. Dunsmore. A matrix class library in $\mathrm{c}++$ for structural engineering computing. Coputers 8 Structures, $55(1): 95-111,1995$.
[^63]: F. Lundh. Python Standard Library. O'Reilly \& Associates, Inc., Sebastopol, CA, USA, 2001.
[^64]: R. I. Mackie. Object oriented programming of the finite element method. International journal for numerical methods in engineering, $35(2): 425-436,1992$.
[^65]: R. I. Mackie. Using objects to handle complexity in finite element software. Engineering with Computers, $13(2): 99-111,1997 .$
[^66]: Maplesoft. Maple's Documentation.
[^67]: T. Mason and D. Brown. Lex 8 yacc. O'Reilly \& Associates, Inc., Sebastopol, CA, USA, $1990 .$
[^68]: MathWorks. Matlab's Documentation.
[^69]: P. Menetrey and T. Zimmermann. Object-oriented non-linear finite element analysis: application to j2 plasticity. Computers $\&$ Structures, $49(5): 767-773,1993$.
[^70]: G. R. Miller. An object-oriented approach to structural analysis and design. Coputers 8 Structures, $40(1): 75-82,1991 .$
[^71]: G. R. Miller. Coordinate-free isoparametric elements. Coputers $\&$ Structures, $49(6): 1027$ 1035,1994 
[^72]: G. R. Miller, S. Banerjee, and K. Sribalaskandarajah. A framework for interactive computational analysis in geomechanics. Computers and Geotechnics, $17(1): 17-37,1995$.
[^73]: J. Mora, R. Otín, P. Dadvand, E. Escolano, M. A. Pasenau, and E. Oñate. Open tools for electromagnetic simulation programs. COMPEL, $25(3): 551-564,2006$.
[^74]: D. Mount and S. Arya. Ann: A library for approximate nearest neighbor searching, $1997 .$
[^75]: E. Oñate. Cálculo de Estructuras por el Método de Elementos Finitos. CIMNE, 2nd edition, $1995 .$
[^76]: E. Oñate, S. Idelson, F. D. Pin, and R. Aubry. The particle finite element method. an overview. International Journal of Computational Methods, $1(2): 267-307,2004 .$
[^77]: Open Engineering. OOFELIE.
[^78]: B. Patzák. OOFEM Documentation. Czech Technical University, Faculty of Civil Engineering, Department of Structural Mechanics.
[^79]: B. Patzák and Z. Bittnar. Object oriented finite element modeling. Acta Polytechnica, $39(2): 99-113,1999 .$
[^80]: R. M. V. Pidaparti and A. V. Hudli. Dynamic analysis of structures using object-oriented techniques. Computers 8 Structures, $49(1): 149-156,1993$.
[^81]: W. H. Press, W. T. Vetterling, S. A. Teukolsky, and B. P. Flannery. Numerical Recipes in C++: The Art of Scientific Computing. Cambridge University Press, $2002 .$
[^82]: B. Raphael and C. S. Krishnamoorthy. Automating finite element development using object oriented techniques. Engineering computations, $10(3): 267-278,1993$.
[^83]: R. Ribó, M. Pasenau, E. Escolano, and J. S. P. Ronda. GiD User Manual. International Center for Numerical Methods in Engineering (CIMNE), Edificio C1, Campus Norte UPC, Gran Capitán $\mathrm{s} / \mathrm{n}, 08034$ Barcelona, Spain.
[^84]: R. Ribó, M. Pasenau, E. Escolano, J. S. P. Ronda, and L. F. González. GiD Reference Manual. International Center for Numerical Methods in Engineering (CIMNE), Edificio C1, Campus Norte UPC, Gran Capitán $\mathrm{s} / \mathrm{n}, 08034$ Barcelona, Spain.
[^85]: R. Rossi. Light weight Structures: Structural Analysis and Coupling Issues. PhD thesis, University of Bologna, 2005 .
[^86]: R. Rossi, S. R. Idelson, and E. Oñate. On the possibilities and validation of the particle finite element method (pfem) for complex engineering fluid flow problems. In Proceedings of ECCOMAS CFD 2006, The Netherlands, 2006 .
[^87]: R. Rossi and R. Vitaliani. Numerical coupled analysis of flexible structures subjected to the fluid action. In 5 th PhD symposium, Delft, 2004 .
[^88]: R. Rossi, R. Vitaliani, and E. Oñate. Validation of a fsi simulation procedure - bridge aerodynamics model problem. In Coupled Problems 2005, Santorini, Italy, 2005 .
[^89]: G. V. Rossum. The Python Language Reference Manual. Network Theory Ltd., 2003 . 
[^90]: Y. Saad. Iterative Methods for Sparse Linear Systems. Society for Industrial and Applied Mathematics, Philadelphia, PA, USA, 2003 .
[^91]: H. Samet. The quadtree and related hierarchical data structures. ACM Comput. Surv., $16(2): 187-260,1984$.
[^92]: H. Samet. The design and analysis of spatial data structures. Addison-Wesley Longman Publishing Co., Inc., Boston, MA, USA, 1990 .
[^93]: H. Si. A Quality Tetrahedral Mesh Generator and Three-Dimensional Delaunay Triangulator (TetGen) User's Manual.
[^94]: J. Šima and P. Orponen. General-purpose computation with neural networks: A survey of complexity theoretic results. Neural Computation, $15(12): 2727-2778$, December 2003 .
[^95]: R. Touzani. OFELI Documentation.
[^96]: R. Touzani. An object oriented finite element toolkit. In Proceedings of the Fifth World Congress on Computational Mechanics (WCCM V), 2002 .
[^97]: T. L. Veldhuizen. Expression templates. $C++$ Report, $7(5): 26-31,1995$.
[^98]: T. L. Veldhuizen. Arrays in blitz++. In Proceedings of the 2nd International Scientific Computing in Object-Oriented Parallel Environments (ISCOPE'98), Lecture Notes in Computer Science. Springer-Verlag, 1998 .
[^99]: T. L. Veldhuizen. C++ templates as partial evaluation. In Partial Evaluation and SemanticBased Program Manipulation, pages 13-18, $1999 .$
[^100]: T. L. Veldhuizen and M. E. Jernigan. Will C++ be faster than Fortran? In Proceedings of the 1st International Scientific Computing in Object-Oriented Parallel Environments (ISCOPE'97), Berlin, Heidelberg, New York, Tokyo, 1997. Springer-Verlag.
[^101]: S. Vinoski. CORBA: integrating diverse applications within distributed heterogeneous environments. IEEE Communications Magazine, $14(2), 1997 .$
[^102]: M. A. Weiss. Data Structures and Algorithm Analysis in $C++$. Addison-Wesley, 3rd edition, 2006 .
[^103]: Wolfram Research. Mathematica's Documentation.
[^104]: O. C. Zienkiewicz and R. L. Taylor. The Finite Element Method. Butterworth-Heinemann, fifth edition, 2000 .
[^105]: T. Zimmermann, P. Bomme, D. Eyheramendy, L. Vernier, and $\mathrm{S}$. Commend. Aspects of an object-oriented finite element environment. Computers and Structures, $68(1): 1-16,1998$.
[^106]: T. Zimmermann, Y. Dubois-Pèlerin, and P. Bomme. Object-oriented finite element programming: I: Governing principles. Comput. Methods Appl. Mech. Eng., $98(2): 291-303,1992$.
[^107]: T. Zimmermann and D. Eyheramendy. Object-oriented finite elements i. principles of symbolic derivations and automatic programming. Comput. Methods Appl. Mech. Eng., $132(3): 259-276(18)$, June 1996 .
